Files
java-design-patterns/onion-architecture

title, shortTitle, description, category, language, tag
title shortTitle description category language tag
Onion Architecture in Java: A Layered Approach to Building Maintainable and Testable Applications Onion Architecture Learn how the Onion Architecture pattern promotes maintainability, testability, and separation of concerns in Java applications. Explore examples, benefits, and best practices. Architectural en
Decoupling
Enterprise patterns
Integration
Microservices
Scalability
Security

Intent of Microservices API Gateway Design Pattern

In this project, the implementation demonstrates Onion Architecture with clear dependency direction: infrastructure and application depend on domain, but domain is independent.

The central intent is to keep business rules in domain, orchestrate use cases in application, and isolate delivery/persistence/API details in infrastructure.

Current implementation highlights:

  • domain: Person, Category, PersonRepository, DomainException
  • application: SavePersonUseCase, GetPersonUseCase, DTO records
  • infrastructure: Spring Boot REST controller, JPA entities, repository adapter, bean wiring

Also known as

  • Ports and Adapters Architecture
  • Hexagonal-style layering (conceptually related)
  • Dependency-rule-first architecture

Detailed Explanation of Onion Architecture Pattern with Real-World Examples

Real-world example

Imagine a people-management service where business validation must stay consistent no matter how data is stored or exposed. In this codebase, Person and Category enforce invariants (e.g., age >= 18, required email/category), use cases coordinate behavior, and infrastructure adapts HTTP + JPA concerns. This allows the persistence or web layer to evolve without changing core domain rules.

In plain words

The project keeps business logic in the center and treats frameworks as replaceable details around it.

Wikipedia says

Onion Architecture is a software architecture pattern that emphasizes separation of concerns and dependency inversion by organizing code in concentric layers, with the domain model at the center.

Sequence diagram

Onion Architecture class diagram

Request flow in this implementation:

  1. Client calls REST endpoint in PersonController (/api/persons, /api/persons/{id})
  2. Controller delegates to SavePersonUseCase or GetPersonUseCase
  3. Use case interacts with PersonRepository abstraction from domain
  4. PersonRepositoryAdapter maps domain <-> JPA and delegates to SpringDataPersonRepository
  5. Response DTO (PersonResponse) is returned to the client

Programmatic Example of Onion Architecture in Java

This repository exposes a simple person API backed by use cases and domain models.

Controller (infrastructure layer):

@RestController
@RequestMapping("/api")
public class PersonController {

    @GetMapping("/persons/{id}")
    public ResponseEntity<PersonResponse> getPerson(@PathVariable Long id) {
        var person = getPersonUseCase.execute(id);
        return ResponseEntity.ok(person);
    }

    @GetMapping("/persons")
    public ResponseEntity<List<PersonResponse>> getAllPersons() {
        var persons = getPersonUseCase.executeAll();
        return ResponseEntity.ok(persons);
    }

    @PostMapping("/persons")
    public ResponseEntity<PersonResponse> savePerson(@RequestBody SavePersonCommand command) {
        try {
            var savedPerson = savePersonUseCase.execute(command);
            return ResponseEntity.status(HttpStatus.OK).body(savedPerson);
        } catch (DomainException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
    }
}

Use case (application layer):

public class SavePersonUseCase {

    private final PersonRepository repository;

    public PersonResponse execute(SavePersonCommand command) {
        var category = new Category(command.categoryId(), command.categoryType());
        var person = new Person(
                null,
                command.firstName(),
                command.lastName(),
                command.age(),
                command.phoneNumber(),
                command.email(),
                category);

        var savedPerson = repository.save(person);
        return new PersonResponse(
                savedPerson.getId(),
                savedPerson.getFirstName(),
                savedPerson.getLastName(),
                savedPerson.getAge(),
                savedPerson.getPhoneNumber(),
                savedPerson.getEmail(),
                savedPerson.getCategory().getId(),
                savedPerson.getCategory().getType()
        );
    }
}

Domain validation (domain layer):

public class Person {
    public Person(Long id, String firstName, String lastName, int age,
                  String phoneNumber, String email, Category category) {
        validateNames(firstName, lastName);
        validateAge(age);
        validatePhone(phoneNumber);
        validateEmail(email);
        validateCategory(category);
        // assign fields...
    }
}

Repository adapter (infrastructure -> domain port):

@Repository
public class PersonRepositoryAdapter implements PersonRepository {

    private final SpringDataPersonRepository repository;

    @Override
    public Optional<Person> findById(Long id) {
        return repository.findById(id).map(this::mapToDomain);
    }

    @Override
    public Person save(Person person) {
        JpaPersonEntity savedEntity = repository.save(mapToEntity(person));
        return mapToDomain(savedEntity);
    }
}
  • Maven 3.6.0 or higher

Build Steps

  1. Navigate to the onion-architecture module directory:

    cd java-design-patterns/onion-architecture
    
  2. Build all modules:

    mvn clean package
    

    This will compile the domain, application, and infrastructure modules and package them into a Spring Boot executable JAR.

  3. Run the Spring Boot application:

    mvn -pl infrastructure spring-boot:run
    

    Alternatively, after building, run the JAR directly:

    java -jar infrastructure/target/infrastructure-1.26.0-SNAPSHOT.jar
    

Accessing the API

The application exposes REST endpoints at http://localhost:8080/api: There is a Postman collection available in the etc/postman folder for testing the API.

  • Get all persons:

    GET http://localhost:8080/api/persons
    
  • Get person by ID:

    GET http://localhost:8080/api/persons/{id}
    
  • Create a new person:

    POST http://localhost:8080/api/persons
    Content-Type: application/json
    
    {
      "firstName": "John",
      "lastName": "Doe",
      "age": 30,
      "phoneNumber": "555-1234",
      "email": "john.doe@example.com",
      "address": "123 Main St",
      "categoryId": 1,
      "categoryType": "individual"
    }
    

Run Tests

To execute unit tests across all modules:

mvn clean test

To run tests for a specific module:

mvn -pl domain test
mvn -pl application test
mvn -pl infrastructure test

Database

The application uses an H2 in-memory database for demonstration purposes. Configuration is in infrastructure/src/main/resources/application.properties:

  • JDBC URL: jdbc:h2:mem:testdb
  • Username: sa
  • Password: password

Sample data is initialized from infrastructure/src/main/resources/data.sql on application startup.

When to Use the Onion Architecture Pattern in Java

  • When domain rules must be stable and independent from frameworks.
  • When you want use cases to be testable without HTTP or database setup.
  • When infrastructure details (web, JPA, database) should be replaceable.
  • When dependency direction must be enforced from outer layers toward the domain core.

Onion Architecture Pattern Java Tutorials

Benefits and Trade-offs of Microservices API Gateway Pattern

Benefits:

  • Business validations are centralized in domain constructors (Person, Category).
  • Use cases stay independent from Spring, JPA, and transport concerns.
  • Repository abstraction (PersonRepository) keeps application logic persistence-agnostic.
  • Testability is strong across layers (domain, use case, adapter, controller tests).

Trade-offs:

  • Additional mapping code between domain models, DTOs, and JPA entities.
  • More classes and modules than a simple CRUD-by-controller approach.
  • Requires discipline to avoid leaking infrastructure concerns into domain/application.

Real-World Applications of Microservices API Gateway Pattern in Java

  • People/contact management services with strict data validation.
  • Internal platforms where multiple delivery mechanisms (REST, batch, messaging) can share the same core domain.
  • Systems that need incremental infrastructure evolution while preserving business logic.
  • Repository - PersonRepository defines the domain-facing persistence contract.
  • Adapter - PersonRepositoryAdapter bridges domain model and Spring Data JPA.
  • Dependency Injection - ApplicationConfig wires use case beans with repository implementations.

References and Credits

  • Project modules: domain, application, infrastructure
  • Java 21 + Maven multi-module setup
  • Spring Boot 3.3 (spring-boot-starter-web, spring-boot-starter-data-jpa, H2)
  • Layer-focused tests in each module validating domain invariants and use case behavior