10 Best Practices for Implementing Hexagonal Architecture in Spring Boot
Hexagonal Architecture isn't just another architectural pattern—it's one of the few approaches that still scales as your codebase, team, and business grow.
Many Spring Boot projects start with clean intentions but gradually become tightly coupled. Business logic leaks into controllers, repositories become the center of the application, and changing one external system unexpectedly impacts the entire codebase.
I've seen this happen repeatedly in enterprise systems.
Hexagonal Architecture (also known as Ports and Adapters) provides a practical solution by making the domain the center of the application, while treating databases, REST APIs, messaging systems, and external services as replaceable implementation details.
However, simply creating packages named domain, application, and infrastructure doesn't mean you've implemented Hexagonal Architecture correctly.
Here are 10 best practices that consistently lead to maintainable Spring Boot applications.
1. Put the Domain at the Center
The domain should contain your business rules—not Spring annotations.
Avoid dependencies like:
@Service
@Component
@Repository
Spring Data
REST APIs
Your domain should be plain Java.
public class Order {
public void confirm() {
if(status == Status.CANCELLED){
throw new IllegalStateException();
}
status = Status.CONFIRMED;
}
}
If your domain cannot be executed without Spring Boot, it isn't truly independent.
Rule: Business logic should survive even if Spring disappears tomorrow.
2. Define Ports Before Writing Adapters
Many developers begin by implementing repositories.
Instead, define what the application actually needs.
Example:
public interface OrderRepositoryPort {
Order save(Order order);
Optional<Order> findById(OrderId id);
}
The domain doesn't care whether data comes from:
PostgreSQL
MongoDB
Redis
DynamoDB
It only knows the port.
Adapters provide the implementation.
3. Keep Application Services Thin
Application services coordinate business use cases.
They should not contain business rules.
Good example:
public class ConfirmOrderUseCase {
private final OrderRepositoryPort repository;
public void execute(OrderId id){
Order order = repository.findById(id)
.orElseThrow();
order.confirm();
repository.save(order);
}
}
Notice where the rule lives:
order.confirm()
—not inside the service.
4. Make Controllers Pure Adapters
Controllers should never contain business logic.
Their responsibilities are limited to:
HTTP mapping
Validation
DTO conversion
Calling the use case
Returning responses
Bad:
if(order.total() > 10000){
...
}
Good:
confirmOrderUseCase.execute(id);
Controllers should be boring.
That's a compliment.
5. Never Expose JPA Entities to the Domain
This is one of the most common mistakes.
Don't let your domain depend on:
@Entity
@Table
@ManyToOne
@OneToMany
Instead:
Domain Model
↓
Mapper
↓
JPA Entity
Persistence becomes an implementation detail.
Changing from JPA to MongoDB should not affect business logic.
6. Use Dependency Injection Only at the Edge
The domain should never know Spring exists.
Instead of:
@Autowired
Prefer constructor injection performed by Spring configuration.
Spring Boot wires dependencies.
The domain remains framework-independent.
7. Separate Incoming and Outgoing Ports
Not every interface is the same.
Incoming Ports:
ConfirmOrderUseCase
CreateCustomerUseCase
CancelPaymentUseCase
Outgoing Ports:
OrderRepositoryPort
PaymentGatewayPort
EmailServicePort
InventoryPort
This separation makes dependencies flow in only one direction.
8. Test the Domain Without Spring Boot
If your unit tests require:
@SpringBootTest
you're probably testing too much.
A domain test should look like:
@Test
void should_confirm_order(){
Order order = new Order();
order.confirm();
assertEquals(Status.CONFIRMED,
order.getStatus());
}
No Spring.
No database.
No container.
Just business logic.
These tests become incredibly fast.
9. Keep Adapters Replaceable
Today you may use:
PostgreSQL
Tomorrow:
CockroachDB
Or perhaps:
Kafka
RabbitMQ
REST API
gRPC
Your application should not care.
Only adapters change.
The core remains untouched.
That's the real power of Hexagonal Architecture.
10. Enforce Dependency Direction
Dependencies should always point inward.
REST Controller
│
▼
Application Layer
│
▼
Domain
▲
│
Infrastructure Adapters
Never allow:
Domain → Spring
Domain → Database
Domain → REST
Domain → Messaging
The center must stay clean.
Common Mistakes
Even experienced teams often make these mistakes:
❌ Putting business logic inside controllers
❌ Letting repositories return JPA entities
❌ Using Spring annotations throughout the domain
❌ Mixing DTOs with domain models
❌ Calling external APIs directly from business logic
❌ Injecting repositories into domain objects
❌ Treating Hexagonal Architecture as merely a package structure
Example Spring Boot Package Structure
src
└── main
└── java
└── com.example.order
├── domain
│ ├── model
│ ├── service
│ └── port
│
├── application
│ ├── usecase
│ └── service
│
├── adapters
│ ├── in
│ │ └── rest
│ │
│ └── out
│ ├── persistence
│ ├── messaging
│ └── external
│
└── configuration
This structure keeps responsibilities clear while making the system easier to evolve as new technologies or integrations are introduced.
Final Thoughts
Hexagonal Architecture is not about adding more layers—it's about reducing coupling.
When implemented well, it enables you to:
Build business logic that is independent of frameworks
Replace infrastructure with minimal impact
Test core functionality quickly and reliably
Scale development across multiple teams
Adapt to changing technologies without rewriting the domain
Spring Boot is an excellent framework, but it should support your architecture—not define it.
A clean architecture ensures your business rules remain stable while everything around them—databases, APIs, messaging systems, and cloud platforms—can evolve over time.
How do you structure your Spring Boot applications?
Do you follow Hexagonal Architecture, Clean Architecture, Layered Architecture, or a hybrid approach? Share your experience in the comments—I'd love to hear what has worked (or not worked) in real production systems.
