Spring Boot Clean Architecture and Hexagonal Design: Ultimate Guide with Full Code

As your Spring Boot applications grow in complexity, so does the importance of maintainable, scalable, and testable architecture. Enter Clean Architecture and Hexagonal Design (Ports & Adapters) β€” two powerful paradigms that help you decouple business logic from infrastructure and delivery layers.

This guide walks you through implementing Spring Boot Clean Architecture Hexagonal Design, using real code examples and structure to build robust and long-lasting systems.

Spring Boot Clean Architecture and Hexagonal Design

🧱 What is Clean Architecture?

Introduced by Robert C. Martin (Uncle Bob), Clean Architecture ensures:

  • Business logic is independent of frameworks
  • Dependencies point inward toward core use cases
  • It’s testable, scalable, and easy to change

πŸ”Ά What is Hexagonal Architecture?

Also known as Ports & Adapters, this architectural style has three main layers:

  1. Domain (Core) – Business rules and models
  2. Application (Use Cases) – Application logic
  3. Adapters (Inbound/Outbound) – Frameworks, databases, REST, messaging

πŸ—οΈ Project Structure


com.kscodes.springboot.advanced
β”‚
β”œβ”€β”€ domain                β†’ Core business logic (no Spring dependencies)
β”‚   β”œβ”€β”€ model
β”‚   └── port
β”‚
β”œβ”€β”€ application           β†’ Use cases / services
β”‚   └── service
β”‚
β”œβ”€β”€ infrastructure        β†’ Adapters (DB, REST, Messaging, etc.)
β”‚   └── repository
β”‚   └── controller
β”‚
└── config                β†’ Spring Configuration

πŸ“¦ Maven Dependencies



    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-data-jpa
    
    
        org.springframework.boot
        spring-boot-starter
    
    
        org.postgresql
        postgresql
    


🧠 1. Domain Layer (Pure Java)

Product.java (Domain Entity)


package com.kscodes.springboot.advanced.domain.model;

public class Product {
    private Long id;
    private String name;

    public Product(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // Getters
}

ProductRepositoryPort.java


package com.kscodes.springboot.advanced.domain.port;

import com.kscodes.springboot.advanced.domain.model.Product;

import java.util.List;
import java.util.Optional;

public interface ProductRepositoryPort {
    List findAll();
    Optional findById(Long id);
    Product save(Product product);
}

🧰 2. Application Layer (Use Cases)

ProductService.java


package com.kscodes.springboot.advanced.application.service;

import com.kscodes.springboot.advanced.domain.model.Product;
import com.kscodes.springboot.advanced.domain.port.ProductRepositoryPort;

import java.util.List;
import java.util.Optional;

public class ProductService {

    private final ProductRepositoryPort repository;

    public ProductService(ProductRepositoryPort repository) {
        this.repository = repository;
    }

    public List getAllProducts() {
        return repository.findAll();
    }

    public Optional getProductById(Long id) {
        return repository.findById(id);
    }

    public Product createProduct(Product product) {
        return repository.save(product);
    }
}

🧱 3. Infrastructure Layer

JpaProductEntity.java


package com.kscodes.springboot.advanced.infrastructure.repository;

import jakarta.persistence.*;

@Entity
@Table(name = "products")
public class JpaProductEntity {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    // Getters and setters
}

SpringDataProductRepository.java


package com.kscodes.springboot.advanced.infrastructure.repository;

import org.springframework.data.jpa.repository.JpaRepository;

public interface SpringDataProductRepository extends JpaRepository {}

JpaProductAdapter.java


package com.kscodes.springboot.advanced.infrastructure.repository;

import com.kscodes.springboot.advanced.domain.model.Product;
import com.kscodes.springboot.advanced.domain.port.ProductRepositoryPort;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Repository
public class JpaProductAdapter implements ProductRepositoryPort {

    private final SpringDataProductRepository repo;

    public JpaProductAdapter(SpringDataProductRepository repo) {
        this.repo = repo;
    }

    @Override
    public List findAll() {
        return repo.findAll().stream()
            .map(e -> new Product(e.getId(), e.getName()))
            .collect(Collectors.toList());
    }

    @Override
    public Optional findById(Long id) {
        return repo.findById(id).map(e -> new Product(e.getId(), e.getName()));
    }

    @Override
    public Product save(Product product) {
        JpaProductEntity entity = new JpaProductEntity();
        entity.setName(product.getName());
        JpaProductEntity saved = repo.save(entity);
        return new Product(saved.getId(), saved.getName());
    }
}

🌐 4. REST Controller (Inbound Adapter)


package com.kscodes.springboot.advanced.infrastructure.controller;

import com.kscodes.springboot.advanced.application.service.ProductService;
import com.kscodes.springboot.advanced.domain.model.Product;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductService service;

    public ProductController(ProductService service) {
        this.service = service;
    }

    @GetMapping
    public List getAll() {
        return service.getAllProducts();
    }

    @GetMapping("/{id}")
    public Optional getById(@PathVariable Long id) {
        return service.getProductById(id);
    }

    @PostMapping
    public Product create(@RequestBody Product product) {
        return service.createProduct(product);
    }
}

βš™οΈ 5. Configuration


package com.kscodes.springboot.advanced.config;

import com.kscodes.springboot.advanced.application.service.ProductService;
import com.kscodes.springboot.advanced.domain.port.ProductRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public ProductService productService(ProductRepositoryPort port) {
        return new ProductService(port);
    }
}

πŸ’‘ Benefits of Clean + Hexagonal Design

BenefitDescription
Framework-agnosticReplace Spring with another library without rewriting domain logic
TestableDomain and services can be tested without databases or web
Clear separationEach layer has a distinct responsibility
AdaptableEasily plug in new adapters (e.g., Kafka, REST, gRPC)

πŸ§ͺ How to Test

  • You can mock the ProductRepositoryPort in unit tests.
  • Integration testing becomes easier with clean separation.
  • Domain logic can be tested as pure Java classes.

πŸ”š Conclusion

Implementing Spring Boot Clean Architecture and Hexagonal Design sets the foundation for long-term code maintainability and system agility. By decoupling domain logic from frameworks and delivery channels, you make your application more resilient to change and future-proof.

Start small β€” modularize your code β€” and watch your app scale without turning into spaghetti.

πŸ”— External References