mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-10-18 12:14:29 +00:00
feat: Add the implementation of Onion-Architecture (#3532)
* Add the implementation of Onion-Architecture * Execute maven command: mvn spotless:apply
This commit is contained in:
1 parent
b55fc2bcb5
commit
688095a4c7
32 files changed
+3182
-1
No files matched your search
@@ -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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user