Skip to main content

Command Palette

Search for a command to run...

10 Best Practices for Implementing Hexagonal Architecture in Spring Boot

Updated
5 min readView as Markdown
B
Senior Software Architect with 30+ years of experience building enterprise systems using Java, Spring Boot, and cloud-native technologies.

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.

1 views

More from this blog

B

Bill LIao's Blog

137 posts

A technical blog on modern backend development, software architecture, and practical AI agent workflows