Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 292 additions & 0 deletions onion-architecture/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
---
title: "Onion Architecture in Java: A Layered Approach to Building Maintainable and Testable Applications"
shortTitle: Onion Architecture
description: "Learn how the Onion Architecture pattern promotes maintainability, testability, and separation of concerns in Java applications. Explore examples, benefits, and best practices."
category: Architectural
language: en
tag:
- 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](./etc/onion-architecture.png)

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):

```java
@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):

```java
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):

```java
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):

```java
@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:**
```bash
cd java-design-patterns/onion-architecture
```

2. **Build all modules:**
```bash
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:**
```bash
mvn -pl infrastructure spring-boot:run
```
Alternatively, after building, run the JAR directly:
```bash
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:**
```bash
GET http://localhost:8080/api/persons
```

- **Get person by ID:**
```bash
GET http://localhost:8080/api/persons/{id}
```

- **Create a new person:**
```bash
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:

```bash
mvn clean test
```

To run tests for a specific module:

```bash
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

* [Clean Architecture with Spring Boot (Baeldung)](https://www.baeldung.com/spring-boot-clean-architecture)
* [Hexagonal Architecture Explained (Cockburn)](https://alistair.cockburn.us/hexagonal-architecture)
* [Spring Data JPA Reference](https://docs.spring.io/spring-data/jpa/reference/)

## 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.

## Related Java Design Patterns

* [Repository](https://martinfowler.com/eaaCatalog/repository.html) - `PersonRepository` defines the domain-facing persistence contract.
* [Adapter](https://refactoring.guru/design-patterns/adapter) - `PersonRepositoryAdapter` bridges domain model and Spring Data JPA.
* [Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html) - `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

66 changes: 66 additions & 0 deletions onion-architecture/application/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).

The MIT License
Copyright © 2014-2022 Ilkka Seppälä

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>onion-architecture</artifactId>
<version>1.26.0-SNAPSHOT</version>
</parent>
<artifactId>application</artifactId>
<name>Application</name>

<dependencies>
<dependency>
<groupId>com.iluwatar</groupId>
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>

<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>

<!-- Mockito for mocking -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
*
* * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
* *
* * The MIT License
* * Copyright © 2014-2022 Ilkka Seppälä
* *
* * Permission is hereby granted, free of charge, to any person obtaining a copy
* * of this software and associated documentation files (the "Software"), to deal
* * in the Software without restriction, including without limitation the rights
* * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* * copies of the Software, and to permit persons to whom the Software is
* * furnished to do so, subject to the following conditions:
* *
* * The above copyright notice and this permission notice shall be included in
* * all copies or substantial portions of the Software.
* *
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* * THE SOFTWARE.
*
*/
package com.iluwatar.onion.application.dto;

public record PersonResponse(
Long id,
String firstName,
String lastName,
int age,
String phoneNumber,
String email,
Long categoryId,
String categoryType) {}
Loading
Loading