-
-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Onion Architecture #3266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Onion Architecture #3266
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
--- | ||
title: "Onion Architecture in Java: Building Maintainable and Scalable Applications" | ||
shortTitle: Onion Architecture | ||
description: "Learn how Onion architecture helps organize your code for better separation of concerns, testability, and long-term maintainability. Ideal for backend developers and software architects building enterprise applications." | ||
category: Architectural | ||
language: en | ||
tag: | ||
- Clean Architecture | ||
- Layered Design | ||
- Dependency Rule | ||
- Maintainability | ||
--- | ||
|
||
## Also known as | ||
|
||
* Ports and Adapters | ||
* Clean Architecture (variant) | ||
|
||
## Intent of Onion Architecture | ||
|
||
The Onion Architecture aims to address common problems in traditional layered architecture by enforcing a clear separation of concerns. It centralizes domain logic and pushes infrastructure concerns to the edges, ensuring that dependencies always point inward toward the domain core. This structure promotes testability, maintainability, and flexibility in adapting to future changes. | ||
|
||
## Detailed Explanation of Onion Architecture with Real-World Example | ||
|
||
Real-world example | ||
|
||
> Imagine a fortress with multiple protective walls around a valuable treasure. The treasure (core domain) is heavily guarded and never exposed directly to the outside world. Visitors (like external systems or users) first pass through the outermost gates (infrastructure), where guards verify their identity. Then they proceed inward through other layers like application services, each with stricter checks, before finally reaching the treasure, but only through clearly defined and controlled pathways. Similarly, in Onion Architecture, the most critical business logic sits protected at the center. External concerns like databases, APIs, or user interfaces are kept at the outer layers, ensuring they cannot tamper directly with the core. Any interaction must pass through proper services and abstractions, preserving the domain’s integrity. | ||
|
||
In plain words | ||
|
||
> Onion Architecture builds your application like an onion: the important core (business rules) stays safe inside, while things like UI and databases are kept outside. No matter how the outer layers change, the core stays stable and unaffected. | ||
|
||
jeffreypalermo.com says | ||
|
||
> The fundamental rule is that all code can depend on layers more central, but code cannot depend on layers further out from the core. In other words, all coupling is toward the center. This architecture is unashamedly biased toward object-oriented programming, and it puts objects before all others. The Onion Architecture relies heavily on the Dependency Inversion principle. | ||
|
||
## Programmatic Example of Onion Architecture in Java | ||
|
||
The Onion Architecture in Java structures code into concentric layers where the core business logic is independent of external concerns like databases, frameworks, or UI. This is achieved by depending on abstractions rather than concrete implementations. | ||
It ensures that the domain remains unaffected even if the technology stack changes, making applications highly maintainable and testable. | ||
|
||
Let's take a look at a simple `OrderService` example in an Onion Architecture style: | ||
|
||
1. Core Domain Layer (domain module) | ||
|
||
```java | ||
public class Order { | ||
private String orderId; | ||
private List<String> items; | ||
|
||
public Order(String orderId, List<String> items) { | ||
this.orderId = orderId; | ||
this.items = items; | ||
} | ||
|
||
public String getOrderId() { | ||
return orderId; | ||
} | ||
|
||
public List<String> getItems() { | ||
return items; | ||
} | ||
} | ||
``` | ||
2. Application Layer (application module) | ||
|
||
```java | ||
public interface OrderRepository { | ||
void save(Order order); | ||
} | ||
``` | ||
|
||
```java | ||
public class OrderService { | ||
private final OrderRepository orderRepository; | ||
|
||
public OrderService(OrderRepository orderRepository) { | ||
this.orderRepository = orderRepository; | ||
} | ||
|
||
public void placeOrder(Order order) { | ||
orderRepository.save(order); | ||
} | ||
} | ||
``` | ||
3. Infrastructure Layer (infrastructure module) | ||
|
||
```java | ||
public class InMemoryOrderRepository implements OrderRepository { | ||
private Map<String, Order> database = new HashMap<>(); | ||
|
||
@Override | ||
public void save(Order order) { | ||
database.put(order.getOrderId(), order); | ||
System.out.println("Order saved: " + order.getOrderId()); | ||
} | ||
} | ||
``` | ||
4. Entry Point (App) | ||
|
||
```java | ||
public class App { | ||
public static void main(String[] args) { | ||
OrderRepository repository = new InMemoryOrderRepository(); | ||
OrderService service = new OrderService(repository); | ||
|
||
List<String> items = Arrays.asList("Book", "Pen"); | ||
Order order = new Order("ORD123", items); | ||
|
||
service.placeOrder(order); | ||
} | ||
} | ||
``` | ||
|
||
- The Domain Layer (Order) has no dependencies. | ||
|
||
- The Application Layer (OrderService, OrderRepository) defines operations abstractly without worrying about how data is stored. | ||
|
||
- The Infrastructure Layer (InMemoryOrderRepository) provides actual data storage. | ||
|
||
- The App Layer wires everything together. | ||
|
||
## When to Use Onion Architecture in Java | ||
|
||
* Enterprise Applications: Perfect for large systems where business logic must remain stable despite frequent changes in technology or external dependencies. | ||
|
||
* Highly Maintainable Systems: Useful when you anticipate frequent feature additions or technology upgrades (e.g., switching from MySQL to MongoDB). | ||
|
||
* Test-Driven Development (TDD): Ideal for systems that require extensive unit and integration testing, as the domain can be tested independently. | ||
|
||
* Microservices: Helps keep microservices clean, with clear separation between core business rules and communication protocols like REST or gRPC. | ||
|
||
## Real-World Applications of Onion Architecture in Java | ||
|
||
* Banking and Financial Systems: Where strict control over domain rules and processes is essential, even when interfacing with different databases, APIs, or UIs. | ||
|
||
* E-commerce Platforms: Separates critical order, payment, and inventory logic from external services like payment gateways and user interfaces. | ||
|
||
* Healthcare Applications: Ensures that patient management, diagnosis, and treatment core logic stays unaffected by changes in reporting tools, hospital systems, or regulatory APIs. | ||
|
||
## Benefits and Trade-offs of Onion Architecture | ||
Benefits: | ||
|
||
* Separation of Concerns: Clear separation between business logic and technical concerns like databases, UI, or external services. | ||
|
||
* High Maintainability: Core business rules can evolve independently of infrastructure or interface changes. | ||
|
||
* Enhanced Testability: Inner layers can be unit-tested easily without setting up external dependencies. | ||
|
||
* Adaptability: Easier to swap out technologies without touching the core domain. | ||
|
||
Trade-offs: | ||
|
||
* Initial Complexity: Requires careful design of layers, abstractions, and dependency rules upfront. | ||
|
||
* More Boilerplate Code: Interfaces, DTOs, and mappers add to codebase size and complexity. | ||
|
||
* Learning Curve: New developers might take longer to understand the structure if they’re not familiar with layered architecture principles. | ||
|
||
## Related Architectural Patterns in Java | ||
|
||
* [Hexagonal Architecture](https://www.geeksforgeeks.org/hexagonal-architecture-system-design/) | ||
* [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) | ||
|
||
## References and Credits | ||
|
||
* [Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software](https://amzn.to/49NGldq) | ||
* [Onion Architecture by Jeffery Palermo](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/) | ||
* [Onion Architecture - Medium article](https://medium.com/expedia-group-tech/onion-architecture-deed8a554423) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import application.TodoService; | ||
import domain.TodoItem; | ||
import infrastructure.TodoRepositoryImpl; | ||
|
||
import java.util.Scanner; | ||
|
||
public class Main { | ||
public static void main(String[] args) { | ||
Scanner scanner = new Scanner(System.in); | ||
TodoService service = new TodoService(new TodoRepositoryImpl()); | ||
|
||
System.out.println("Welcome to the TODO App!"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Lombok's @slf4j logging utility |
||
|
||
boolean running = true; | ||
while (running) { | ||
System.out.println("\nWhat would you like to do?"); | ||
System.out.println("[a] Add tasks"); | ||
System.out.println("[d] Mark tasks as done"); | ||
System.out.println("[v] View tasks"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add "delete task" function as well? |
||
System.out.println("[q] Quit"); | ||
System.out.print("> "); | ||
String choice = scanner.nextLine().trim().toLowerCase(); | ||
|
||
switch (choice) { | ||
case "a": | ||
System.out.println("Enter your tasks (type 'q' to stop adding):"); | ||
while (true) { | ||
System.out.print("> "); | ||
String input = scanner.nextLine().trim(); | ||
if (input.equalsIgnoreCase("q")) break; | ||
if (!input.isEmpty()) { | ||
service.createTodo(input); | ||
} | ||
} | ||
break; | ||
|
||
case "d": | ||
if (service.getTodos().isEmpty()) { | ||
System.out.println("📝 No tasks to mark as done."); | ||
break; | ||
} | ||
System.out.println("\nEnter the ID(s) of tasks to mark as done (comma separated, or 'q' to cancel):"); | ||
System.out.print("> "); | ||
String idsInput = scanner.nextLine().trim(); | ||
if (!idsInput.equalsIgnoreCase("q")) { | ||
String[] parts = idsInput.split(","); | ||
for (String part : parts) { | ||
try { | ||
int id = Integer.parseInt(part.trim()); | ||
service.completeTodo(id); | ||
} catch (NumberFormatException e) { | ||
System.out.println("Invalid ID: " + part); | ||
} | ||
} | ||
} | ||
break; | ||
|
||
case "v": | ||
System.out.println("\n📋 Your Todo List:"); | ||
if (service.getTodos().isEmpty()) { | ||
System.out.println("No tasks yet!"); | ||
} else { | ||
for (TodoItem item : service.getTodos()) { | ||
String status = item.isCompleted() ? "✅ Done" : "❌ Not Done"; | ||
System.out.println(item.getId() + ": " + item.getTitle() + " [" + status + "]"); | ||
} | ||
} | ||
break; | ||
|
||
case "q": | ||
running = false; | ||
break; | ||
|
||
default: | ||
System.out.println("Unknown option. Please try again."); | ||
} | ||
} | ||
|
||
System.out.println("\nGoodbye! Here's your final Todo List:"); | ||
|
||
if(service.getTodos().size() == 0) | ||
System.out.println("No tasks left!"); | ||
|
||
else { | ||
for (TodoItem item : service.getTodos()) { | ||
String status = item.isCompleted() ? "✅ Done" : "❌ Not Done"; | ||
System.out.println(item.getId() + ": " + item.getTitle() + " [" + status + "]"); | ||
} | ||
} | ||
|
||
scanner.close(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package application; | ||
|
||
import domain.TodoItem; | ||
import domain.TodoRepository; | ||
|
||
import java.util.List; | ||
|
||
public class TodoService { | ||
private final TodoRepository repository; | ||
private static int idCounter = 1; | ||
public TodoService(TodoRepository repository) { | ||
this.repository = repository; | ||
} | ||
|
||
public void createTodo(String title) { | ||
TodoItem item = new TodoItem(idCounter++, title); | ||
repository.add(item); | ||
} | ||
|
||
public List<TodoItem> getTodos() { | ||
return repository.getAll(); | ||
} | ||
|
||
public void completeTodo(int id) { | ||
repository.markAsCompleted(id); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package domain; | ||
|
||
public class TodoItem { | ||
private int id; | ||
private String title; | ||
private boolean isCompleted; | ||
|
||
public TodoItem(int id, String title) { | ||
this.id = id; | ||
this.title = title; | ||
this.isCompleted = false; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use Lombok's @AllArgsConstructor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... and @Getter |
||
|
||
public int getId() { return id; } | ||
public String getTitle() { return title; } | ||
public boolean isCompleted() { return isCompleted; } | ||
|
||
public void markCompleted() { | ||
this.isCompleted = true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package domain; | ||
|
||
import java.util.List; | ||
|
||
public interface TodoRepository { | ||
void add(TodoItem item); | ||
List<TodoItem> getAll(); | ||
void markAsCompleted(int id); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package infrastructure; | ||
|
||
import domain.TodoItem; | ||
import domain.TodoRepository; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
public class TodoRepositoryImpl implements TodoRepository { | ||
private final List<TodoItem> todos = new ArrayList<>(); | ||
|
||
@Override | ||
public void add(TodoItem item) { | ||
todos.add(item); | ||
} | ||
|
||
@Override | ||
public List<TodoItem> getAll() { | ||
return new ArrayList<>(todos); | ||
} | ||
|
||
@Override | ||
public void markAsCompleted(int id) { | ||
for (TodoItem item : todos) { | ||
if (item.getId() == id) { | ||
item.markCompleted(); | ||
break; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package application; | ||
|
||
import application.TodoService; | ||
import domain.TodoItem; | ||
import infrastructure.TodoRepositoryImpl; | ||
|
||
import java.util.List; | ||
|
||
public class TodoServiceTest { | ||
public static void main(String[] args) { | ||
TodoRepositoryImpl fakeRepo = new TodoRepositoryImpl(); | ||
TodoService service = new TodoService(fakeRepo); | ||
|
||
service.createTodo("Learn onion architecture"); | ||
service.createTodo("Write unit tests"); | ||
|
||
List<TodoItem> todos = service.getTodos(); | ||
|
||
assert todos.size() == 2 : "Should have 2 todos"; | ||
assert todos.get(0).getTitle().equals("Learn onion architecture"); | ||
assert !todos.get(0).isCompleted(); | ||
|
||
int idToComplete = todos.get(0).getId(); | ||
service.completeTodo(idToComplete); | ||
|
||
todos = service.getTodos(); | ||
assert todos.get(0).isCompleted() : "First item should be completed"; | ||
|
||
System.out.println("TodoServiceTest passed"); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't look like a valid JUnit test. You should mark it with @test annotation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here above the
Main
, explain the pattern and describe how this code example implements it.