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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Mastering Hexagonal Architecture in Spring Boot: 3 Implementation Strategies

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 classes

2.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 tasks

3.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/                     # Configuration

4.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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

software architectureDomain-Driven DesignHexagonal Architecture
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.