Mastering Hexagonal Architecture in Spring Boot: 3 Implementation Strategies
This article explains the fundamentals of hexagonal (ports‑and‑adapters) architecture, presents three ways to implement it in Spring Boot—including a classic version, a DDD‑enhanced version, and a simplified variant—while analyzing their advantages, disadvantages, and suitable scenarios.
1. Hexagonal Architecture Fundamentals
Hexagonal architecture, also known as ports and adapters or onion architecture, is a design pattern that decouples business logic from external dependencies.
1.1 Core Concepts
Proposed by Alistair Cockburn in 2005, its core idea is to isolate the internal domain logic from the outside world. The architecture consists of three parts:
Domain : contains business logic and domain models, the core of the application.
Ports : define interfaces through which the application interacts with the outside.
Adapters : implement the port interfaces, connecting external systems to the application.
1.2 Port Classification
Ports are usually divided into two categories:
Primary/Driving Ports (Input Ports) : allow external systems to drive the application, e.g., REST APIs, CLI.
Secondary/Driven Ports (Output Ports) : allow the application to drive external systems, e.g., databases, message queues, third‑party services.
1.3 Advantages of Hexagonal Architecture
Business Logic Independence : core logic does not depend on any specific framework.
Testability : business logic can be tested without external dependencies.
Flexibility : technology implementations can be swapped without affecting the core.
Separation of Concerns : clear distinction between business rules and technical details.
Maintainability : clearer code structure eases maintenance and extension.
2. Classic Hexagonal Architecture Implementation
2.1 Project Structure
src/main/java/com/example/demo/
├── domain/ # Domain layer
│ ├── model/ # Domain models
│ ├── service/ # Domain services
│ └── port/ # Port definitions
│ ├── incoming/ # Input ports
│ └── outgoing/ # Output ports
├── adapter/ # Adapter layer
│ ├── incoming/ # Input adapters
│ │ ├── rest/ # REST API adapters
│ │ └── scheduler/ # Scheduler adapters
│ └── outgoing/ # Output adapters
│ ├── persistence/ # Persistence adapters
│ └── messaging/ # Messaging adapters
└── application/ # Application configuration
└── config/ # Spring configuration classes2.2 Code Implementation
2.2.1 Domain Model
// Domain model
package com.example.demo.domain.model;
public class Product {
private Long id;
private String name;
private double price;
private int stock;
// constructors, getters, setters omitted for brevity
// domain behavior
public boolean isAvailable() {
return stock > 0;
}
public void decreaseStock(int quantity) {
if (quantity > stock) {
throw new IllegalArgumentException("Not enough stock");
}
this.stock -= quantity;
}
}2.2.2 Input Port (Use‑case Interface)
// Input port (use‑case interface)
package com.example.demo.domain.port.incoming;
import com.example.demo.domain.model.Product;
import java.util.List;
import java.util.Optional;
public interface ProductService {
List<Product> getAllProducts();
Optional<Product> getProductById(Long id);
Product createProduct(Product product);
void updateStock(Long productId, int quantity);
}2.2.3 Output Port (Repository Interface)
// Output port (repository interface)
package com.example.demo.domain.port.outgoing;
import com.example.demo.domain.model.Product;
import java.util.List;
import java.util.Optional;
public interface ProductRepository {
List<Product> findAll();
Optional<Product> findById(Long id);
Product save(Product product);
void deleteById(Long id);
}2.2.4 Domain Service Implementation
// Domain service implementation
package com.example.demo.domain.service;
import com.example.demo.domain.model.Product;
import com.example.demo.domain.port.incoming.ProductService;
import com.example.demo.domain.port.outgoing.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
public ProductServiceImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@Override
public Optional<Product> getProductById(Long id) {
return productRepository.findById(id);
}
@Override
public Product createProduct(Product product) {
return productRepository.save(product);
}
@Override
@Transactional
public void updateStock(Long productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.decreaseStock(quantity);
productRepository.save(product);
}
}2.2.5 Input Adapter (REST API)
// REST adapter
package com.example.demo.adapter.incoming.rest;
import com.example.demo.domain.model.Product;
import com.example.demo.domain.port.incoming.ProductService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productService.getProductById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public Product createProduct(@RequestBody Product product) {
return productService.createProduct(product);
}
@PutMapping("/{id}/stock")
public ResponseEntity<Void> updateStock(@PathVariable Long id, @RequestParam int quantity) {
productService.updateStock(id, quantity);
return ResponseEntity.ok().build();
}
}2.2.6 Output Adapter (Persistence)
// JPA entity
package com.example.demo.adapter.outgoing.persistence.entity;
import javax.persistence.*;
@Entity
@Table(name = "products")
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
private int stock;
// constructors, getters, setters omitted
}
// JPA repository
package com.example.demo.adapter.outgoing.persistence.repository;
import com.example.demo.adapter.outgoing.persistence.entity.ProductEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface JpaProductRepository extends JpaRepository<ProductEntity, Long> {}
// Persistence adapter
package com.example.demo.adapter.outgoing.persistence;
import com.example.demo.adapter.outgoing.persistence.entity.ProductEntity;
import com.example.demo.adapter.outgoing.persistence.repository.JpaProductRepository;
import com.example.demo.domain.model.Product;
import com.example.demo.domain.port.outgoing.ProductRepository;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
public class ProductRepositoryAdapter implements ProductRepository {
private final JpaProductRepository jpaRepository;
public ProductRepositoryAdapter(JpaProductRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public List<Product> findAll() {
return jpaRepository.findAll().stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
@Override
public Optional<Product> findById(Long id) {
return jpaRepository.findById(id)
.map(this::toDomain);
}
@Override
public Product save(Product product) {
ProductEntity entity = toEntity(product);
ProductEntity saved = jpaRepository.save(entity);
return toDomain(saved);
}
@Override
public void deleteById(Long id) {
jpaRepository.deleteById(id);
}
private Product toDomain(ProductEntity entity) {
Product product = new Product();
product.setId(entity.getId());
product.setName(entity.getName());
product.setPrice(entity.getPrice());
product.setStock(entity.getStock());
return product;
}
private ProductEntity toEntity(Product product) {
ProductEntity entity = new ProductEntity();
entity.setId(product.getId());
entity.setName(product.getName());
entity.setPrice(product.getPrice());
entity.setStock(product.getStock());
return entity;
}
}2.3 Pros and Cons Analysis
Advantages
Clear structure strictly follows hexagonal principles.
Domain model is completely independent of any framework.
Adapters isolate all external dependencies.
Highly testable; external components can be easily mocked.
Disadvantages
More code is required; many interfaces and adapters must be written.
Object‑mapping work increases; conversion between domain and persistence objects is needed.
May feel like over‑engineering for simple applications.
Steep learning curve; the team must deeply understand hexagonal architecture.
2.4 Suitable Scenarios
Complex business domains that need clear separation of rules.
Core systems with long‑term maintenance requirements.
Teams already familiar with hexagonal principles.
Projects that require flexible replacement of technical implementations.
3. Hexagonal Architecture with DDD
3.1 Project Structure
src/main/java/com/example/demo/
├── domain/ # Domain layer
│ ├── model/ # Domain models
│ │ ├── aggregate/ # Aggregates
│ │ ├── entity/ # Entities
│ │ └── valueobject/ # Value objects
│ ├── service/ # Domain services
│ └── repository/ # Repository interfaces
├── application/ # Application layer
│ ├── port/ # Application service ports
│ │ ├── incoming/ # Input ports
│ │ └── outgoing/ # Output ports
│ └── service/ # Application service implementations
├── infrastructure/ # Infrastructure layer
│ ├── adapter/ # Adapters
│ │ ├── incoming/ # Input adapters
│ │ └── outgoing/ # Output adapters
│ └── config/ # Configuration classes
└── interface/ # Interface layer
├── rest/ # REST APIs
├── graphql/ # GraphQL APIs
└── scheduler/ # Scheduled tasks3.2 Code Implementation
3.2.1 Value Object Example
// Value object
package com.example.demo.domain.model.valueobject;
import java.math.BigDecimal;
public class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// other methods omitted
}3.2.2 Entity Example
// Entity
package com.example.demo.domain.model.entity;
import com.example.demo.domain.model.valueobject.Money;
public class Product {
private ProductId id;
private String name;
private Money price;
private int stock;
// constructors, getters, setters omitted
public boolean isAvailable() {
return stock > 0;
}
public void decreaseStock(int quantity) {
if (quantity > stock) {
throw new IllegalArgumentException("Not enough stock");
}
this.stock -= quantity;
}
}3.2.3 Aggregate Root Example
// Aggregate root
package com.example.demo.domain.model.aggregate;
import com.example.demo.domain.model.entity.Product;
import com.example.demo.domain.model.valueobject.Money;
import java.util.ArrayList;
import java.util.List;
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderLine> orderLines = new ArrayList<>();
private OrderStatus status;
private Money totalAmount;
// constructors, getters, setters omitted
public void addProduct(Product product, int quantity) {
if (!product.isAvailable() || product.getStock() < quantity) {
throw new IllegalArgumentException("Product not available");
}
OrderLine line = new OrderLine(product.getId(), product.getPrice(), quantity);
orderLines.add(line);
recalculateTotal();
}
public void confirm() {
if (orderLines.isEmpty()) {
throw new IllegalStateException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
}
private void recalculateTotal() {
this.totalAmount = orderLines.stream()
.map(OrderLine::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}3.2.4 Repository Interface
// Repository interface
package com.example.demo.domain.repository;
import com.example.demo.domain.model.aggregate.Order;
import com.example.demo.domain.model.aggregate.OrderId;
import java.util.Optional;
public interface OrderRepository {
Optional<Order> findById(OrderId id);
Order save(Order order);
void delete(OrderId id);
}3.2.5 Application Service Interface
// Application service interface
package com.example.demo.application.port.incoming;
import com.example.demo.application.dto.OrderRequest;
import com.example.demo.application.dto.OrderResponse;
import java.util.List;
import java.util.Optional;
public interface OrderApplicationService {
OrderResponse createOrder(OrderRequest request);
Optional<OrderResponse> getOrder(String orderId);
List<OrderResponse> getCustomerOrders(String customerId);
void confirmOrder(String orderId);
}3.2.6 Application Service Implementation
// Application service implementation
package com.example.demo.application.service;
import com.example.demo.application.dto.OrderRequest;
import com.example.demo.application.dto.OrderResponse;
import com.example.demo.application.port.incoming.OrderApplicationService;
import com.example.demo.application.port.outgoing.ProductRepository;
import com.example.demo.domain.model.aggregate.Order;
import com.example.demo.domain.model.aggregate.OrderId;
import com.example.demo.domain.model.entity.Product;
import com.example.demo.domain.repository.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class OrderApplicationServiceImpl implements OrderApplicationService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
public OrderApplicationServiceImpl(OrderRepository orderRepository, ProductRepository productRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
}
@Override
@Transactional
public OrderResponse createOrder(OrderRequest request) {
Order order = new Order(new CustomerId(request.getCustomerId()));
for (OrderRequest.OrderItem item : request.getItems()) {
Product product = productRepository.findById(new ProductId(item.getProductId()))
.orElseThrow(() -> new RuntimeException("Product not found"));
order.addProduct(product, item.getQuantity());
product.decreaseStock(item.getQuantity());
productRepository.save(product);
}
Order saved = orderRepository.save(order);
return mapToDto(saved);
}
@Override
public Optional<OrderResponse> getOrder(String orderId) {
return orderRepository.findById(new OrderId(orderId))
.map(this::mapToDto);
}
@Override
public List<OrderResponse> getCustomerOrders(String customerId) {
return orderRepository.findByCustomerId(new CustomerId(customerId)).stream()
.map(this::mapToDto)
.collect(Collectors.toList());
}
@Override
@Transactional
public void confirmOrder(String orderId) {
Order order = orderRepository.findById(new OrderId(orderId))
.orElseThrow(() -> new RuntimeException("Order not found"));
order.confirm();
orderRepository.save(order);
}
private OrderResponse mapToDto(Order order) {
// mapping logic omitted for brevity
return null;
}
}3.2.7 Input Adapter (REST Controller)
// REST controller
package com.example.demo.interface.rest;
import com.example.demo.application.dto.OrderRequest;
import com.example.demo.application.dto.OrderResponse;
import com.example.demo.application.port.incoming.OrderApplicationService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderApplicationService orderService;
public OrderController(OrderApplicationService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
OrderResponse response = orderService.createOrder(request);
return ResponseEntity.ok(response);
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable String orderId) {
return orderService.getOrder(orderId)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/customer/{customerId}")
public List<OrderResponse> getCustomerOrders(@PathVariable String customerId) {
return orderService.getCustomerOrders(customerId);
}
@PostMapping("/{orderId}/confirm")
public ResponseEntity<Void> confirmOrder(@PathVariable String orderId) {
orderService.confirmOrder(orderId);
return ResponseEntity.ok().build();
}
}3.2.8 Output Adapter (JPA Persistence)
// JPA entity for Order
package com.example.demo.infrastructure.adapter.outgoing.persistence.entity;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
@Id
private String id;
private String customerId;
@Enumerated(EnumType.STRING)
private OrderStatusJpa status;
private BigDecimal totalAmount;
private String currency;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "order_id")
private List<OrderLineJpaEntity> orderLines = new ArrayList<>();
// constructors, getters, setters omitted
}
// JPA repository
package com.example.demo.infrastructure.adapter.outgoing.persistence.repository;
import com.example.demo.infrastructure.adapter.outgoing.persistence.entity.OrderJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface OrderJpaRepository extends JpaRepository<OrderJpaEntity, String> {
List<OrderJpaEntity> findByCustomerId(String customerId);
}
// Adapter implementation
package com.example.demo.infrastructure.adapter.outgoing.persistence;
import com.example.demo.domain.model.aggregate.Order;
import com.example.demo.domain.model.aggregate.OrderId;
import com.example.demo.domain.model.entity.CustomerId;
import com.example.demo.domain.repository.OrderRepository;
import com.example.demo.infrastructure.adapter.outgoing.persistence.entity.OrderJpaEntity;
import com.example.demo.infrastructure.adapter.outgoing.persistence.repository.OrderJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Repository
public class OrderRepositoryAdapter implements OrderRepository {
private final OrderJpaRepository jpaRepository;
public OrderRepositoryAdapter(OrderJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepository.findById(id.getValue())
.map(this::toDomain);
}
@Override
public Order save(Order order) {
OrderJpaEntity entity = toJpaEntity(order);
OrderJpaEntity saved = jpaRepository.save(entity);
return toDomain(saved);
}
@Override
public void delete(OrderId id) {
jpaRepository.deleteById(id.getValue());
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return jpaRepository.findByCustomerId(customerId.getValue()).stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
private Order toDomain(OrderJpaEntity entity) {
// mapping logic omitted
return null;
}
private OrderJpaEntity toJpaEntity(Order order) {
// mapping logic omitted
return null;
}
}3.3 Pros and Cons Analysis
Advantages
Combines DDD concepts, providing richer domain models.
Expresses business rules and constraints more precisely.
Domain model is completely separated from persistence.
Supports complex business scenarios and domain behavior.
Disadvantages
Architecture complexity increases further.
Steep learning curve; developers must master both DDD and hexagonal architecture.
Object‑mapping workload becomes heavier.
Risk of over‑design for simple domains.
3.4 Suitable Scenarios
Complex business domains with rich rules and constraints.
Large enterprise core systems.
Teams already familiar with DDD and hexagonal principles.
Long‑term maintained systems that need to adapt to business changes.
4. Simplified Hexagonal Implementation
4.1 Project Structure
src/main/java/com/example/demo/
├── service/ # Service layer
│ ├── business/ # Business services
│ ├── model/ # Data models
│ └── exception/ # Business exceptions
├── integration/ # Integration layer
│ ├── database/ # Database integration
│ ├── messaging/ # Messaging integration
│ └── external/ # External service integration
├── web/ # Web layer
│ ├── controller/ # Controllers
│ ├── dto/ # Data Transfer Objects
│ └── advice/ # Global exception handling
└── config/ # Configuration4.2 Code Implementation
4.2.1 Data Model
// Business model
package com.example.demo.service.model;
import lombok.Data;
@Data
public class Product {
private Long id;
private String name;
private double price;
private int stock;
public boolean isAvailable() {
return stock > 0;
}
public void decreaseStock(int quantity) {
if (quantity > stock) {
throw new IllegalArgumentException("Not enough stock");
}
this.stock -= quantity;
}
}4.2.2 Integration Layer Interface
// Database integration interface
package com.example.demo.integration.database;
import com.example.demo.service.model.Product;
import java.util.List;
import java.util.Optional;
public interface ProductRepository {
List<Product> findAll();
Optional<Product> findById(Long id);
Product save(Product product);
void deleteById(Long id);
}4.2.3 Business Service
// Business service
package com.example.demo.service.business;
import com.example.demo.integration.database.ProductRepository;
import com.example.demo.service.model.Product;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> getAllProducts() {
return productRepository.findAll();
}
public Optional<Product> getProductById(Long id) {
return productRepository.findById(id);
}
public Product createProduct(Product product) {
return productRepository.save(product);
}
@Transactional
public void updateStock(Long productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.decreaseStock(quantity);
productRepository.save(product);
}
}4.2.4 Controller
// REST controller
package com.example.demo.web.controller;
import com.example.demo.service.business.ProductService;
import com.example.demo.service.model.Product;
import com.example.demo.web.dto.ProductDTO;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<ProductDTO> getAllProducts() {
return productService.getAllProducts().stream()
.map(this::toDto)
.collect(Collectors.toList());
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProductById(@PathVariable Long id) {
return productService.getProductById(id)
.map(this::toDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ProductDTO createProduct(@RequestBody ProductDTO productDto) {
Product product = toEntity(productDto);
return toDto(productService.createProduct(product));
}
@PutMapping("/{id}/stock")
public ResponseEntity<Void> updateStock(@PathVariable Long id, @RequestParam int quantity) {
productService.updateStock(id, quantity);
return ResponseEntity.ok().build();
}
private ProductDTO toDto(Product product) {
ProductDTO dto = new ProductDTO();
dto.setId(product.getId());
dto.setName(product.getName());
dto.setPrice(product.getPrice());
dto.setStock(product.getStock());
return dto;
}
private Product toEntity(ProductDTO dto) {
Product product = new Product();
product.setId(dto.getId());
product.setName(dto.getName());
product.setPrice(dto.getPrice());
product.setStock(dto.getStock());
return product;
}
}4.2.5 Database Implementation
// JPA entity (similar to business model)
package com.example.demo.integration.database.entity;
import lombok.Data;
import javax.persistence.*;
@Entity
@Table(name = "products")
@Data
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
private int stock;
}
// JPA repository
package com.example.demo.integration.database.repository;
import com.example.demo.integration.database.entity.ProductEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface JpaProductRepository extends JpaRepository<ProductEntity, Long> {}
// Repository implementation
package com.example.demo.integration.database;
import com.example.demo.integration.database.entity.ProductEntity;
import com.example.demo.integration.database.repository.JpaProductRepository;
import com.example.demo.service.model.Product;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Repository
public class ProductRepositoryImpl implements ProductRepository {
private final JpaProductRepository jpaRepository;
public ProductRepositoryImpl(JpaProductRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public List<Product> findAll() {
return jpaRepository.findAll().stream()
.map(this::toModel)
.collect(Collectors.toList());
}
@Override
public Optional<Product> findById(Long id) {
return jpaRepository.findById(id)
.map(this::toModel);
}
@Override
public Product save(Product product) {
ProductEntity entity = toEntity(product);
return toModel(jpaRepository.save(entity));
}
@Override
public void deleteById(Long id) {
jpaRepository.deleteById(id);
}
private Product toModel(ProductEntity entity) {
Product product = new Product();
product.setId(entity.getId());
product.setName(entity.getName());
product.setPrice(entity.getPrice());
product.setStock(entity.getStock());
return product;
}
private ProductEntity toEntity(Product product) {
ProductEntity entity = new ProductEntity();
entity.setId(product.getId());
entity.setName(product.getName());
entity.setPrice(product.getPrice());
entity.setStock(product.getStock());
return entity;
}
}4.3 Pros and Cons Analysis
Advantages
Simple structure, low learning curve.
Fewer interfaces and layers, resulting in less code.
Follows Spring conventions, friendly to Spring developers.
High development efficiency, suitable for rapid iteration.
Still maintains basic separation between business logic and external dependencies.
Disadvantages
Separation is not as strict as the classic hexagonal version.
Business logic may mix with non‑core concerns.
Domain model is less rich.
Limited support for complex business scenarios.
4.4 Suitable Scenarios
Small‑to‑medium applications with relatively simple business logic.
Projects that need fast development and iteration.
Prototypes or MVPs.
Early‑stage projects that may evolve to a stricter architecture later.
5. Conclusion
The core value of hexagonal architecture lies in separating business logic from technical details, thereby improving maintainability, testability, and flexibility.
Regardless of which implementation style is chosen, the principle of keeping the domain model pure and the boundaries clear should always be upheld.
Architecture should serve the business, not the other way around. Selecting the appropriate approach should aim to boost development efficiency, system quality, and business adaptability.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
