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.

π§± 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:
- Domain (Core) β Business rules and models
- Application (Use Cases) β Application logic
- 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
| Benefit | Description |
|---|---|
| Framework-agnostic | Replace Spring with another library without rewriting domain logic |
| Testable | Domain and services can be tested without databases or web |
| Clear separation | Each layer has a distinct responsibility |
| Adaptable | Easily plug in new adapters (e.g., Kafka, REST, gRPC) |
π§ͺ How to Test
- You can mock the
ProductRepositoryPortin 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.