Skip to content
ArticlesBack EndJava

Hexagonal Architecture vs. Clean Architecture vs. Onion Architecture

17 min readsalahtobok
Hexagonal, Clean, and Onion Architecture Comparison

In modern enterprise software engineering, building applications that survive technology shifts is a constant challenge. Frameworks evolve, database drivers change, and cloud infrastructure moves rapidly. If your core business logic is tightly coupled to Spring annotations, Hibernate entities, or REST controllers, updating infrastructure becomes risky, expensive, and slow.

To solve this fundamental coupling challenge, software engineers created three influential architectural styles: Hexagonal Architecture (Ports and Adapters), Onion Architecture, and Clean Architecture. While each model introduces distinct vocabulary and diagrammatic conventions, they all share a singular goal: isolating the business domain from technical mechanisms. In this comprehensive guide, we compare all three architectures, examine their common foundation, analyze practical code in Spring Boot 4 and Java 25, and provide an objective decision framework for enterprise systems.

Table of Contents

The Core Dilemma: Why Traditional Layered Architecture Fails

For more than two decades, the standard pattern for Java web development was the three-tier layered architecture: Controller Layer, Service Layer, and Data Access / Repository Layer. In this traditional model, dependencies point downwards:

Controller -> Service -> Repository -> Database

While intuitive, this linear dependency chain creates serious long-term architectural decay in enterprise projects:

  • Database-Driven Design: Because the Service layer directly references the Repository layer, database concerns dictate domain modeling. Developers design relational tables and Hibernate entities first, then build business logic around database schemas rather than pure business rules.
  • Pervasive Framework Contamination: JPA annotations (such as @Entity, @Table, @JoinColumn) and Jackson annotations leak directly into domain objects. The business logic cannot run without Spring, Hibernate, and an active database connection.
  • Brittle Unit Testing: Testing business logic requires initializing Spring ApplicationContext, configuring in-memory databases (H2), or managing complex Mockito mock chains. Tests become slow, difficult to maintain, and fragile.
  • High Refactoring Costs: Replacing a relational database with a document store, splitting services into microservices, or adopting an asynchronous messaging broker (like Apache Kafka) requires rewriting core business classes across every layer.

Hexagonal, Clean, and Onion architectures dismantle this top-down dependency chain by applying the Dependency Inversion Principle (DIP): high-level business policies must never depend on low-level technical details; both must depend on abstractions.

Hexagonal Architecture (Ports and Adapters) Deep Dive

Formulated by Alistair Cockburn in 2005, Hexagonal Architecture (formally known as the Ports and Adapters pattern) was designed to solve a specific problem: applications could only interact through their graphical user interface and database, making automated testing difficult.

Hexagonal Architecture (Ports and Adapters) showing Application Core, Inbound Ports, and Outbound Adapters
Figure 1: Hexagonal Architecture (Ports and Adapters) isolating domain logic from delivery mechanisms and external infrastructure.

The hexagon shape is symbolic: it does not imply six sides or six components. Cockburn chose a polygon to visually represent multiple connection points (ports) through which the application interacts with the external world.

Core Concepts in Hexagonal Architecture

  1. The Application Core (Inside): Contains pure domain entities, business rules, and use case services. The inside has zero dependencies on any external framework, database, or UI technology.
  2. Ports (The Boundary): A port is a language-level interface that defines a protocol of interaction. Ports belong entirely to the application core.
    • Inbound (Driving / Primary) Ports: Interfaces that define what the application can do (e.g., CreateUserUseCase, SubmitOrderCommand). Driving actors (REST controllers, CLI commands, Kafka consumers) call these ports.
    • Outbound (Driven / Secondary) Ports: Interfaces that define what the application needs from external systems (e.g., UserRepositoryPort, PaymentGatewayPort, NotificationPort).
  3. Adapters (Outside): Concrete implementations of ports that translate external technologies into domain concepts and vice versa.
    • Inbound Adapters: Spring Web MVC controllers, GraphQL resolvers, or queue listeners that receive HTTP requests, parse JSON payloads, and invoke an inbound port.
    • Outbound Adapters: Classes that implement outbound ports (e.g., JpaUserRepositoryAdapter implementing UserRepositoryPort using Spring Data JPA).

Clean Architecture: Concentric Circles and the Dependency Rule

Published by Robert C. Martin (“Uncle Bob”) in 2012, Clean Architecture consolidated Hexagonal Architecture, Onion Architecture, and DCI (Data, Context, and Interaction) into a unified visual model structured around concentric circular layers.

Clean Architecture Concentric Circles showing Entities, Use Cases, Interface Adapters, and Frameworks
Figure 2: Clean Architecture concentric circle topology enforcing the Dependency Rule strictly toward the inner core.

The foundational premise of Clean Architecture is The Dependency Rule: Source code dependencies must point only inward, toward higher-level policies. Nothing in an inner circle can know anything at all about something in an outer circle. This includes functions, classes, variables, and data formats.

The Four Concentric Circles of Clean Architecture

  1. Entities (Yellow Core): Enterprise-wide business rules. Entities encapsulate critical business data and business invariants. They can be plain Java objects, records, or rich domain classes. They are completely independent of UI, database, or application frameworks.
  2. Use Cases (Red Ring): Application-specific business rules. Use cases coordinate the flow of data to and from entities, directing them to use their enterprise business rules to achieve the goals of the use case (e.g., PlaceOrderUseCase).
  3. Interface Adapters (Green Ring): Translators and presenters. This layer converts data from the format most convenient for the use cases and entities into the format most convenient for external agencies (such as databases or the web). Controllers, presenters, and gateways reside here.
  4. Frameworks and Drivers (Blue Outer Rim): The outermost layer composed of tools, databases, web frameworks (like Spring Boot), message queues, and devices. This layer contains glue code that connects external technical tools to the interface adapters.

Onion Architecture: Domain Services and Concentric Isolation

Introduced by Jeffrey Palermo in 2008, Onion Architecture was created specifically to counter the database-centric coupling of traditional layered enterprise systems in object-oriented applications.

Onion Architecture Layers showing Domain Model, Domain Services, Application Services, and Infrastructure
Figure 3: Onion Architecture structure placing the Domain Model at the core, wrapped by Domain Services and Application Services.

Onion Architecture explicitly distinguishes between Domain Services (business logic that operates across multiple entities within the domain) and Application Services (coordinating workflow, transactions, security, and use case execution).

Layers of the Onion

  1. Domain Model: At the very center of the onion. Contains stateful domain entities, value objects, and business rules. It has zero external dependencies.
  2. Domain Services: Sits directly around the Domain Model. Houses domain-specific logic that naturally involves multiple domain entities or requires complex business calculations that do not belong inside a single entity.
  3. Application Services: Surrounds the Domain Services. Coordinates application use cases, security contexts, transaction boundaries, and logging. It declares interfaces for external infrastructure (such as repository interfaces and mail sender interfaces).
  4. Infrastructure / Outer Ring: Contains implementations of interfaces declared in inner rings: database repositories, web API controllers, cloud file storage adapters, and external microservice clients.

Side-by-Side Structural and Terminology Comparison

Engineers often debate whether their project is “Hexagonal,” “Clean,” or “Onion.” In practice, these architectures are semantic variations of the same architectural philosophy. They use different vocabulary to describe identical boundary concepts.

Structural Comparison of Hexagonal, Clean, and Onion Architecture Topologies
Figure 4: Side-by-side architectural comparison illustrating terminology differences, layer mapping, and shared dependency inversion mechanics.

The following mapping table translates the terminology across all three styles:

Architectural Role Hexagonal (Cockburn) Clean Architecture (Martin) Onion Architecture (Palermo)
Core Business Rules Domain / Model Enterprise Business Rules (Entities) Domain Model Core
Cross-Entity Business Logic Domain Services Domain Services Domain Services Layer
Application Use Cases Application / Inside Service Use Cases Layer Application Services Layer
Inbound Contract Inbound / Driving Port Use Case Input Port Application Service Interface
Outbound Contract Outbound / Driven Port Gateway / Output Port Interface Repository / Infrastructure Interface
Inbound Trigger Driving Adapter (Controller) Interface Adapter (Controller) UI / API Controller (Outer Ring)
Outbound Implementation Driven Adapter (JPA Repo) Frameworks / DB Adapter Infrastructure Implementation

The Unified Dependency Inversion Rule

Despite their aesthetic differences—Cockburn’s hexagon, Martin’s concentric colored rings, and Palermo’s onion layers—all three architectures enforce the exact same universal law of software design: The Unified Dependency Inversion Rule.

The Unified Dependency Inversion Rule across Hexagonal, Clean, and Onion Architectures
Figure 5: The unified dependency inversion synthesis showing pure domain entities at the core, use case ports, and inbound/outbound adapters.

This universal architectural synthesis establishes three invariant principles:

  1. The Domain is Sovereign: Business rules have zero dependencies on frameworks, databases, or communication protocols. If Spring Boot or PostgreSQL ceased to exist tomorrow, your domain model code would compile without modification.
  2. Dependencies Point Inward: Outer layers depend on inner layers. Inner layers never depend on outer layers. Code in the domain layer must never import packages from infrastructure, controllers, or database frameworks.
  3. Abstractions Are Owned by the Caller: When the core domain requires external services (such as persisting an entity or sending an email), it defines an interface (a Port or SPI) in its own package. The infrastructure layer implements that interface. Control flows outward at runtime, but source code dependencies point strictly inward.

Practical Implementation in Spring Boot 4 and Java 25

Our companion workshop project demonstrates all three architectures within a clean Spring Boot 4 and Java 25 environment. Below, we examine concrete implementations of each pattern.

1. Hexagonal Architecture Implementation (Ports & Adapters)

In Hexagonal Architecture, the domain defines the User entity and the UserRepositoryPort. The use case implements the CreateUserUseCase inbound port:

// Inbound Port: Application contract for creating a user
package com.webcodein.architecture.hexagonal.port.in;

import com.webcodein.architecture.hexagonal.domain.User;

public interface CreateUserUseCase {
    User createUser(String id, String name);
}
// Outbound Port: Secondary SPI defined by the domain
package com.webcodein.architecture.hexagonal.port.out;

import com.webcodein.architecture.hexagonal.domain.User;

public interface UserRepositoryPort {
    void save(User user);
    User findById(String id);
}
// Driving Adapter: Spring MVC Controller invoking Inbound Port
package com.webcodein.architecture.hexagonal.adapter.in;

import com.webcodein.architecture.hexagonal.domain.User;
import com.webcodein.architecture.hexagonal.port.in.CreateUserUseCase;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/hex/users")
public class UserController {

    private final CreateUserUseCase createUserUseCase;

    public UserController(CreateUserUseCase createUserUseCase) {
        this.createUserUseCase = createUserUseCase;
    }

    @PostMapping
    public ResponseEntity<User> create(@RequestBody User user) {
        User created = createUserUseCase.createUser(user.getId(), user.getName());
        return ResponseEntity.ok(created);
    }
}

2. Clean Architecture Implementation (Use Case Interactors)

Clean Architecture organizes workflows into Use Case Interactors that accept request models and return response models:

// Use Case Interactor coordinating entity business rules
package com.webcodein.architecture.clean.usecase;

import com.webcodein.architecture.clean.domain.Order;

public class CreateOrderUseCase {

    private final OrderRepositoryGateway repositoryGateway;

    public CreateOrderUseCase(OrderRepositoryGateway repositoryGateway) {
        this.repositoryGateway = repositoryGateway;
    }

    public Order execute(String id) {
        Order order = new Order(id, "CREATED");
        repositoryGateway.save(order);
        return order;
    }
}

3. Onion Architecture Implementation (Core Domain Services)

Onion Architecture emphasizes the domain service operating over core entities:

// Core Domain Service in Onion Architecture
package com.webcodein.architecture.onion.domain;

public class ProductService {

    private final ProductRepository repository;

    public ProductService(ProductRepository repository) {
        this.repository = repository;
    }

    public Product createProduct(String id) {
        Product product = new Product(id, "ACTIVE");
        repository.save(product);
        return product;
    }
}

Comparative Analysis: Architectural Strategies Matrix

To help software architects select the right architectural strategy for their systems, the following matrix compares six architectural patterns across key enterprise metrics:

Architecture Style Boilerplate Overhead Domain Isolation Refactoring Agility Testability Velocity Ideal Enterprise Workload
Traditional 3-Tier Layered Very Low (Direct mapping) Low (DB coupled) Low (Cascading breaks) Moderate (Requires Spring mocks) Simple CRUD microservices, rapid internal prototypes, throwaway proofs of concept.
Hexagonal (Ports & Adapters) Moderate (Ports & Adapters) High (Pure core) Very High (Interchangeable adapters) Very High (Zero framework mocks) Services with multiple protocols (HTTP REST, gRPC, Kafka consumers, CLI tools).
Onion Architecture Moderate (Layer separation) High (Domain service focus) High (Infrastructure swappable) High (Isolated domain logic) Domain-heavy enterprise systems with complex calculation engines and rich entity state.
Clean Architecture High (Interactors & Presenters) Very High (Concentric rings) Very High (Complete UI/DB decoupling) Very High (Pure unit tests) Long-lived enterprise platforms with high regulatory compliance and multi-team collaboration.
Modular Monolith (Spring Modulith) Low (Package boundaries) High (Domain events) High (Monolith-to-microservice path) Very High (Slice integration tests) Medium-to-large business applications looking to avoid distributed system operational cost.
Event-Driven CQRS (Axon / Kafka) High (Command/Query split) Very High (Immutable events) High (Independent read scaling) Moderate (Async event replay) Financial transaction ledgers, audit systems, inventory order fulfillment platforms.

Real-World Enterprise Scenarios

To understand when each architectural approach excels, consider three real-world enterprise deployments:

1. Core Banking Financial Ledger (Hexagonal Architecture)

A retail bank must process payments originating from diverse channels: SWIFT financial messages, mobile banking REST APIs, ATM ISO-8583 connections, and nightly batch processing files. The core ledger business rules (double-entry bookkeeping, account debit limits, currency rounding invariants) must execute identically regardless of whether the transaction arrived via an ATM socket or a REST call.

Why Hexagonal Excels: The core accounting domain defines a single Inbound Port: PostTransactionUseCase. Multiple driving adapters (REST Controller, ISO-8583 Socket Listener, SWIFT MQ Consumer) convert incoming payloads into standard domain commands. Outbound ports decouple the domain from Oracle, PostgreSQL, or audit Kafka streams.

2. Multi-Cloud E-Commerce Order Platform (Clean Architecture)

A global retailer operates in hybrid cloud environments across AWS, Azure, and on-premises data centers. To avoid cloud vendor lock-in, the engineering management mandates that order placement, inventory reservations, and tax calculations must be completely independent of AWS DynamoDB, Azure Cosmos DB, or specific cloud messaging systems.

Why Clean Architecture Excels: Clean Architecture enforces rigorous use-case boundaries. The PlaceOrderUseCase interactor accepts input data transfer objects, interacts with pure domain entities, and outputs through presenter interfaces. The persistence layer implements cloud-specific database adapters without leaking cloud SDK annotations into business code.

3. Healthcare Clinical Patient Records Gateway (Onion Architecture)

A medical healthcare software system manages electronic health records (EHR) across hospitals. Clinical dosage calculations and patient allergy contraindications involve complex business rules across multiple domain entities (Patient, Medication, Diagnosis, LabResult).

Why Onion Architecture Excels: Onion Architecture’s dedicated Domain Services layer provides a natural home for cross-entity clinical validation rules that do not belong inside an individual Patient or Medication entity. Application services handle hospital audit logging and HIPAA compliance encryption without compromising domain logic.

Architectural Guardrails and ArchUnit Enforcement

The greatest risk to decoupled architectures is human error: a developer accidentally imports a Spring framework class or a JPA entity inside the domain package. Over time, architectural boundaries erode until the codebase degrades back into a tangled monolith.

Automated architectural fitness tests using ArchUnit prevent this erosion by failing the build whenever a boundary rule is violated. Below is an essential ArchUnit test verifying Hexagonal and Clean Architecture rules:

package com.webcodein.architecture;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(packages = "com.webcodein.architecture", importOptions = ImportOption.DoNotIncludeTests.class)
public class ArchitectureBoundaryRulesTest {

    @ArchTest
    public static final ArchRule domainMustNotDependOnFrameworks =
        noClasses().that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAnyPackage(
                "org.springframework..",
                "jakarta.persistence..",
                "jakarta.servlet..",
                "com.fasterxml.jackson.."
            )
            .because("The domain model must remain purely decoupled from external frameworks and persistence details.");

    @ArchTest
    public static final ArchRule domainMustNotDependOnAdapters =
        noClasses().that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAPackage("..adapter..")
            .because("Source code dependencies must point inward; domain classes must never reference adapters.");

    @ArchTest
    public static final ArchRule adaptersMustImplementPorts =
        classes().that().resideInAPackage("..adapter.out..")
            .should().implement(classes().that().resideInAPackage("..port.out.."))
            .because("Outbound secondary adapters must implement domain-defined outbound ports.");
}

Running this test during Maven builds guarantees that architectural boundaries remain strictly enforced across pull requests and team changes.

Common Pitfalls and How to Fix Them

When migrating or implementing decoupled architectures, development teams frequently fall victim to three anti-patterns:

Pitfall 1: Leaking Persistence Annotations into the Domain

Symptom: Developers place JPA annotations (@Entity, @Id, @OneToMany) directly onto domain classes to avoid writing separate database entities and mappers. Over time, JPA lifecycle quirks (lazy loading exceptions, dirty checking, cascading deletes) contaminate business logic.

Solution: Strictly separate Domain Entities from Persistence Entities. Maintain a pure domain class in the domain package, and a separate UserJpaEntity in the outbound adapter package. Use an explicit mapper or MapStruct:

// Pure domain entity: No JPA annotations, full business validation
public class User {
    private final UserId id;
    private final String name;

    public User(UserId id, String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("User name must not be blank.");
        }
        this.id = id;
        this.name = name;
    }
    // Business methods...
}

Pitfall 2: Over-Abstraction with Useless Pass-Through Layers

Symptom: For a simple read operation, a developer creates an Inbound Port, a Use Case Interactor, an Outbound Port, an Adapter, and a Repository, with each layer doing nothing more than calling the next layer without business logic.

Solution: Apply CQRS (Command Query Responsibility Segregation). For complex write operations with business invariants, use full Ports and Adapters. For simple read queries that only fetch data for the UI, bypass use case interactors and let read adapters query the database directly using projections.

Pitfall 3: The Anemic Domain Model Anti-Pattern

Symptom: Domain entities become dumb data holders containing only getters and setters, while all business logic resides in huge procedural Service or Use Case classes.

Solution: Keep domain logic inside the domain entities. Entities must enforce their own business invariants, state transitions, and validation rules. The use case interactor should only orchestrate the loading of the entity, invoking its business methods, and saving the updated entity via the repository port.

Testing Strategies: Unit Isolation vs. Slice Tests

One of the greatest advantages of Hexagonal, Clean, and Onion architectures is testing velocity. In traditional layered architectures, developers are forced to write heavy @SpringBootTest integration tests for simple logic. In decoupled architectures, testing aligns with the pyramid:

  1. Pure Unit Tests for Domain Entities: Test core business rules in milliseconds using standard JUnit 5. No Spring context, no database, no mocks.
  2. Use Case Tests with In-Memory Mocks: Test use case interactors using simple in-memory repository implementations (e.g., a HashMap backed port implementation). This tests the entire application workflow in under 10 milliseconds without Spring.
  3. Adapter Slice Tests:
    • Test Inbound Adapters (REST Controllers) using Spring Boot’s lightweight @WebMvcTest.
    • Test Outbound Adapters (JPA Repositories) using real PostgreSQL Testcontainers via @DataJpaTest.

Frequently Asked Questions

Which architecture should I choose for a new Spring Boot 4 project?

For most enterprise applications, Hexagonal Architecture (Ports and Adapters) offers the most pragmatic balance between architectural purity and development speed. Its distinction between driving and driven ports maps cleanly to Spring Boot controllers and repository beans without the excessive presenter layers found in Clean Architecture.

Do I need separate DTOs for every layer?

Yes, at architectural boundaries. Inbound adapters should map HTTP JSON requests to domain command objects, and outbound adapters should map domain entities to database records. Sharing a single entity across all layers reintroduces the tight coupling that these architectures were specifically designed to prevent.

Does Hexagonal Architecture impact runtime performance?

No. The mapping between DTOs and domain objects takes nanoseconds in memory. Furthermore, because domain logic is completely isolated from the database, you can optimize caching, connection pooling, and virtual thread concurrency in the adapter layer without touching business logic.

Can I combine Hexagonal Architecture with Domain-Driven Design (DDD)?

Absolutely. Hexagonal Architecture is the primary architectural style recommended by Domain-Driven Design practitioners. DDD provides the tactical patterns (Entities, Value Objects, Aggregates, Domain Events) to model the inside of the hexagon, while Ports and Adapters provide the outer delivery boundaries.

How does Spring Modulith relate to Clean or Hexagonal Architecture?

Spring Modulith enforces modular boundaries at the aggregate or module level within a Spring Boot monolith. You can structure individual modules internally using Hexagonal Architecture (Ports and Adapters), while using Spring Modulith to enforce communication between modules via asynchronous domain events.

Conclusion and Next Steps

Hexagonal Architecture, Clean Architecture, and Onion Architecture are different expressions of the same timeless engineering wisdom: protect the core business domain from volatile technical dependencies.

By placing your business rules at the center, defining strict inbound and outbound ports, and inverting source code dependencies, you build applications that easily withstand framework upgrades, database migrations, and operational shifts. Combined with Spring Boot 4 and Java 25, these architectures provide the foundation for robust, verifiable, and long-lasting enterprise software.

To deepen your software architecture and modern Java skills, explore our related engineering deep dives across the WebCodein ecosystem:

You can find the complete, runnable Spring Boot 4 companion project for this guide on GitHub:

GitHub: WebCodein Workshop — Hexagonal vs Clean vs Onion Architecture

References

Comments