๐Ÿ—๏ธ Hexagonal Architecture in Java: Structuring APIs for Maintainability

After exploring Quarkus performance, let’s talk about an equally crucial aspect: how to properly structure your API code. Hexagonal architecture (or “Ports & Adapters”) isn’t just a theoretical concept - it’s a pragmatic approach that transforms the maintainability of your Java projects.

๐ŸŽฏ The Problem with Traditional Architectures

How many times have you seen Spring Boot projects that end up like this:

  • Fat Controllers: Business logic mixed with HTTP handling
  • Anemic Services: Just CRUD operations that map to the database
  • Tight Coupling: Impossible to test without starting the entire application
  • Technical Debt: Each new feature becomes more complicated to implement
  • Fragile Tests: Changing one dependency breaks all tests

The problem? Business logic is scattered everywhere, coupled to technical details (database, HTTP, etc.). Result: code that’s difficult to maintain and evolve.

๐Ÿ”„ Hexagonal Architecture: Clear Separation

The Fundamental Principle

The idea is simple: isolate business logic at the center, and decouple everything else (database, REST API, messaging, etc.) via interfaces.

1
2
3
4
5
6
7
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Controllers   โ”‚โ”€โ”€โ”€โ–ถโ”‚   Use Cases     โ”‚โ”€โ”€โ”€โ–ถโ”‚   Repositories  โ”‚
โ”‚   (Adapters)    โ”‚    โ”‚   (Domain)      โ”‚    โ”‚   (Adapters)    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚                       โ”‚                       โ”‚
        โ–ผ                       โ–ผ                       โ–ผ
   HTTP/REST              Business Logic            Database

Concrete Benefits

โœ… Isolated Tests: Test your business logic without starting the app
โœ… Flexibility: Change database without impacting business logic
โœ… Scalability: Easily add new entry channels (GraphQL, gRPC, etc.)
โœ… Maintainability: Organized code, clear responsibilities
โœ… Reusability: Business logic can be reused in different contexts

๐Ÿ›๏ธ Concrete Structure of a Hexagonal Project

Package Organization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
org.acme.demo/
โ”œโ”€โ”€ domain/
โ”‚   โ”œโ”€โ”€ model/          # Business entities
โ”‚   โ”œโ”€โ”€ port/           # Interfaces (ports)
โ”‚   โ””โ”€โ”€ service/        # Use cases / Business services
โ”œโ”€โ”€ infrastructure/
โ”‚   โ”œโ”€โ”€ adapter/
โ”‚   โ”‚   โ”œโ”€โ”€ in/         # Incoming adapters (REST, GraphQL)
โ”‚   โ”‚   โ””โ”€โ”€ out/        # Outgoing adapters (DB, External APIs)
โ”‚   โ””โ”€โ”€ config/         # Spring/Quarkus configuration
โ””โ”€โ”€ application/        # Application entry point

Layers Explained

Domain (the core):

  • Entities: Business objects with their rules
  • Ports: Interfaces defining contracts
  • Use Cases: Pure business logic, no technical dependencies

Infrastructure (the periphery):

  • Adapters In: REST Controllers, GraphQL endpoints, message consumers
  • Adapters Out: JPA Repositories, HTTP clients, message publishers
  • Config: Dependency injection, technical configuration

๐Ÿ› ๏ธ Concrete Example with Quarkus

1. Business Entity (Domain)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// domain/model/User.java
public class User {
    private final UserId id;
    private final Email email;
    private final UserStatus status;
    
    // Business logic in the entity
    public void activate() {
        if (this.status == UserStatus.SUSPENDED) {
            throw new BusinessException("Cannot activate suspended user");
        }
        this.status = UserStatus.ACTIVE;
    }
}

2. Port (Domain Interface)

1
2
3
4
5
6
// domain/port/UserRepository.java
public interface UserRepository {
    Optional<User> findById(UserId id);
    void save(User user);
    List<User> findByStatus(UserStatus status);
}

3. Use Case (Business Logic)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// domain/service/ActivateUserUseCase.java
@ApplicationScoped
public class ActivateUserUseCase {
    private final UserRepository userRepository;
    
    public void execute(UserId userId) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
        
        user.activate(); // Business logic in the entity
        userRepository.save(user);
    }
}

4. REST Adapter (Infrastructure)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// infrastructure/adapter/in/UserController.java
@RestController
@Path("/users")
public class UserController {
    private final ActivateUserUseCase activateUserUseCase;
    
    @POST
    @Path("/{id}/activate")
    public Response activateUser(@PathParam("id") String id) {
        activateUserUseCase.execute(new UserId(id));
        return Response.ok().build();
    }
}

5. Repository Adapter (Infrastructure)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// infrastructure/adapter/out/JpaUserRepository.java
@ApplicationScoped
public class JpaUserRepository implements UserRepository {
    @Inject
    EntityManager em;
    
    @Override
    public Optional<User> findById(UserId id) {
        UserEntity entity = em.find(UserEntity.class, id.getValue());
        return entity != null ? Optional.of(entity.toDomain()) : Optional.empty();
    }
}

๐Ÿงช Testing: The Real Advantage

Use Case Unit Test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Test
void should_activate_user_when_user_exists() {
    // Given
    User user = new User(userId, email, UserStatus.INACTIVE);
    when(userRepository.findById(userId)).thenReturn(Optional.of(user));
    
    // When
    activateUserUseCase.execute(userId);
    
    // Then
    verify(userRepository).save(argThat(u -> u.getStatus() == UserStatus.ACTIVE));
}

Benefits:

  • No application startup
  • Fast tests (< 100ms)
  • Isolated business logic
  • Simple dependency mocking

Integration Test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@QuarkusTest
class UserControllerIT {
    @Test
    void should_activate_user_via_rest_api() {
        given()
            .when().post("/users/123/activate")
            .then().statusCode(200);
        
        // Verify in database that user is activated
    }
}

๐Ÿ’ก Quarkus + Hexagonal Architecture: The Winning Combo

Why They Work Well Together?

Native Dependency Injection:

  • @ApplicationScoped for use cases
  • @Inject for automatic port injection
  • Annotation-based configuration, no XML

Performance:

  • Native compilation preserves architecture
  • Fast startup even with complex structure
  • Reflection eliminated, optimized interfaces

Integrated Testing:

  • @QuarkusTest for integration tests
  • @TestProfile for different test environments
  • Native Mockito mocking

๐Ÿ“Š Ideal Use Cases

Complex APIs with Rich Business Logic

  • E-commerce applications (order management, pricing, inventory)
  • Management systems (CRM, ERP, etc.)
  • APIs with evolving business rules

Multi-Team Projects

  • Clear separation of responsibilities
  • Domain team vs infrastructure team
  • Facilitated integration

Applications with Multiple Channels

  • REST + GraphQL + gRPC
  • Batch + API + Events
  • Progressive migration from legacy systems

โš ๏ธ When NOT to Use It?

Simple CRUD

For a basic API without complex business logic, it might be overkill. A simple Controller โ†’ Service โ†’ Repository pattern might suffice.

Rapid Prototypes

For a POC or demo, the complete structure might slow down initial development.

Junior Teams

There’s a learning curve. The team needs to be trained in DDD and hexagonal architecture concepts.

๐Ÿš€ Getting Started

Steps to Begin

  1. Identify your business domain: What are your main entities?
  2. Define your use cases: What does your application actually do?
  3. Create the ports: What interfaces does your domain need?
  4. Implement the adapters: REST, database, etc.
  5. Test by layer: Domain โ†’ Use Cases โ†’ Adapters

Progressive Migration

No need to refactor everything at once:

  • Start with new features
  • Gradually isolate existing business logic
  • Refactor the most critical parts

๐Ÿ’ก Concrete Example

To see all these concepts in action, check out the demonstration repository that implements a complete Quarkus API with hexagonal architecture:

๐Ÿ”— GitHub Repository - Quarkus Demo API

๐Ÿ”ฎ What’s Next?

Domain Driven Design (DDD)

Hexagonal architecture pairs perfectly with DDD:

  • Aggregates, Value Objects, Domain Events
  • Bounded contexts for large projects
  • Event Sourcing for historization

CQRS (Command Query Responsibility Segregation)

Separate commands (write) from queries (read):

  • Command use cases vs read use cases
  • Separate performance optimization
  • Improved scalability

๐ŸŽ‰ In Summary

Hexagonal architecture + Quarkus means:

  • Maintainable and scalable code
  • Fast and reliable tests
  • Technological flexibility
  • Preserved performance

Yes, it requires a bit more initial effort. But in the long run, you gain enormously in productivity and peace of mind.

Hexagonal architecture isn’t just an academic pattern - it’s a pragmatic tool for robust and durable APIs.

Are you already testing it? Share your feedback!