Skip to main content

Command Palette

Search for a command to run...

Step-by-Step: Implementing Hexagonal Architecture in Spring Boot

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

"Good architecture isn't about making your code more complicated. It's about making future change less expensive."

As software systems grow, one problem appears over and over again.

Business logic becomes tightly coupled to frameworks, databases, REST APIs, messaging systems, and external services.

Eventually, every change becomes risky.

Need to replace MySQL with PostgreSQL? Unexpected refactoring.

Need to expose GraphQL instead of REST? Business logic changes.

Need to introduce Kafka? Half the application gets modified.

This isn't a Spring Boot problem.

It's an architecture problem.

One of the most effective ways to solve it is Hexagonal Architecture (also called Ports and Adapters).

After applying it in multiple enterprise systems, I've found that it dramatically improves maintainability, testability, and long-term flexibility.

Here's a practical step-by-step guide to implementing it in Spring Boot.


What Is Hexagonal Architecture?

Hexagonal Architecture was introduced by Alistair Cockburn.

Its central idea is simple:

Business logic should not depend on external technologies. External technologies should depend on the business logic.

Instead of building applications around Spring, JPA, REST, or Kafka, we build them around the domain.

Everything else becomes replaceable.

Think of your application as a hexagon.

Outside the hexagon are adapters:

  • REST APIs

  • Databases

  • Message brokers

  • External services

  • Command-line tools

Inside the hexagon lives only business logic.

The business logic doesn't know or care who calls it.


Step 1: Define the Domain

The domain contains:

  • Entities

  • Value Objects

  • Business Rules

  • Domain Services

Example:

public class Order {

    private OrderId id;
    private CustomerId customerId;
    private List<OrderItem> items;

    public void confirm() {
        // business rules
    }
}

Notice what's missing.

No:

  • @Entity

  • @Autowired

  • @Component

  • Spring annotations

  • JPA imports

The domain is pure Java.

This makes it easy to test and independent of any framework.


Step 2: Define Input Ports (Use Cases)

A port represents what the application can do.

Example:

public interface CreateOrderUseCase {

    Order create(CreateOrderCommand command);

}

This is the API of your application.

Controllers call this interface.

Tests call this interface.

Even scheduled jobs call this interface.

Nobody depends on implementation details.


Step 3: Define Output Ports

Business logic often needs external systems.

Instead of calling repositories directly, define interfaces.

Example:

public interface OrderRepository {

    Order save(Order order);

    Optional<Order> findById(OrderId id);

}

Notice:

The domain defines the interface.

Infrastructure implements it.

Dependency direction remains inward.


Step 4: Implement the Use Case

Application services coordinate business logic.

@Service
public class CreateOrderService
        implements CreateOrderUseCase {

    private final OrderRepository repository;

    public Order create(CreateOrderCommand command) {

        Order order = Order.create(command);

        return repository.save(order);

    }
}

Notice something interesting.

This service doesn't know:

  • JPA

  • SQL

  • PostgreSQL

  • MongoDB

It only knows the repository interface.


Step 5: Build the REST Adapter

Now expose the use case through HTTP.

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final CreateOrderUseCase useCase;

}

The controller translates:

HTTP → Domain

That's all.

No business rules should live here.


Step 6: Build the Database Adapter

Infrastructure implements the output port.

@Repository
public class JpaOrderRepository
        implements OrderRepository {

}

Internally, it may use:

  • Spring Data JPA

  • Hibernate

  • JDBC

  • MyBatis

The domain never notices.

Tomorrow you can replace JPA entirely.


Step 7: Add Messaging

Suppose you publish events.

Instead of:

kafkaTemplate.send(...)

inside business logic...

Define another output port.

public interface EventPublisher {

    void publish(OrderCreatedEvent event);

}

Infrastructure implements it.

KafkaEventPublisher

Later you can replace Kafka with RabbitMQ or AWS SNS without touching business logic.


Step 8: Organize the Project Structure

A common Spring Boot layout looks like this:

src
 ├── domain
 │     ├── model
 │     ├── ports
 │     └── services
 │
 ├── application
 │     ├── usecases
 │     └── services
 │
 ├── adapters
 │     ├── inbound
 │     │      ├── rest
 │     │      └── messaging
 │     │
 │     └── outbound
 │            ├── persistence
 │            ├── kafka
 │            └── clients
 │
 └── config

The separation becomes very clear.


Step 9: Testing Becomes Easy

Testing business logic becomes trivial.

Instead of starting Spring Boot...

Instead of connecting databases...

Instead of mocking HTTP...

Simply mock the ports.

OrderRepository repository = mock(...);

CreateOrderService service =
    new CreateOrderService(repository);

Tests become:

  • Fast

  • Deterministic

  • Independent

Many complete in milliseconds.


Step 10: Replace Infrastructure Without Fear

Imagine these changes:

✅ REST → GraphQL

✅ MySQL → PostgreSQL

✅ Kafka → RabbitMQ

✅ Local Storage → AWS S3

✅ SMTP → SendGrid

In a layered architecture, these changes often ripple through multiple layers.

In Hexagonal Architecture, most changes stay confined to adapters.

The core business logic remains untouched.

That's the real payoff.


Common Mistakes

I've reviewed many projects claiming to use Hexagonal Architecture but making these mistakes:

❌ JPA annotations inside domain entities

❌ Business logic inside controllers

❌ Spring dependencies everywhere

❌ Repository interfaces defined in infrastructure

❌ Domain objects returning HTTP responses

❌ Services calling Kafka directly

If your domain knows about Spring, it isn't truly framework-independent.


When Should You Use It?

Hexagonal Architecture is an excellent fit for:

  • Enterprise applications

  • Microservices

  • Financial systems

  • Healthcare platforms

  • Long-lived products

  • Complex business domains

It may be unnecessary for:

  • Small CRUD applications

  • Short-lived prototypes

  • Internal tools with limited complexity

Architecture should match the problem—not every project needs maximum abstraction.


Final Thoughts

Hexagonal Architecture isn't about adding layers for the sake of elegance.

It's about protecting what changes the least—your business rules—from what changes the most—frameworks, databases, APIs, and infrastructure.

Spring Boot will continue to evolve.

Databases will change.

Cloud providers will change.

Messaging platforms will change.

But your business logic should remain stable.

Design your application so that technology is a plugin, not the foundation.

That's the essence of Hexagonal Architecture.

3 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