Skip to content

Commit d91d3d8

Browse files
Sri TalluriSri Talluri
Sri Talluri
authored and
Sri Talluri
committed
Onion Architecture
1 parent 356db48 commit d91d3d8

File tree

8 files changed

+402
-0
lines changed

8 files changed

+402
-0
lines changed

onion-architecture/README.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
title: "Onion Architecture in Java: Building Maintainable and Scalable Applications"
3+
shortTitle: Onion Architecture
4+
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."
5+
category: Architectural
6+
language: en
7+
tag:
8+
- Clean Architecture
9+
- Layered Design
10+
- Dependency Rule
11+
- Maintainability
12+
---
13+
14+
## Also known as
15+
16+
* Ports and Adapters
17+
* Clean Architecture (variant)
18+
19+
## Intent of Onion Architecture
20+
21+
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.
22+
23+
## Detailed Explanation of Onion Architecture with Real-World Example
24+
25+
Real-world example
26+
27+
> 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.
28+
29+
In plain words
30+
31+
> 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.
32+
33+
jeffreypalermo.com says
34+
35+
> 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.
36+
37+
## Programmatic Example of Onion Architecture in Java
38+
39+
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.
40+
It ensures that the domain remains unaffected even if the technology stack changes, making applications highly maintainable and testable.
41+
42+
Let's take a look at a simple `OrderService` example in an Onion Architecture style:
43+
44+
1. Core Domain Layer (domain module)
45+
46+
```java
47+
public class Order {
48+
private String orderId;
49+
private List<String> items;
50+
51+
public Order(String orderId, List<String> items) {
52+
this.orderId = orderId;
53+
this.items = items;
54+
}
55+
56+
public String getOrderId() {
57+
return orderId;
58+
}
59+
60+
public List<String> getItems() {
61+
return items;
62+
}
63+
}
64+
```
65+
2. Application Layer (application module)
66+
67+
```java
68+
public interface OrderRepository {
69+
void save(Order order);
70+
}
71+
```
72+
73+
```java
74+
public class OrderService {
75+
private final OrderRepository orderRepository;
76+
77+
public OrderService(OrderRepository orderRepository) {
78+
this.orderRepository = orderRepository;
79+
}
80+
81+
public void placeOrder(Order order) {
82+
orderRepository.save(order);
83+
}
84+
}
85+
```
86+
3. Infrastructure Layer (infrastructure module)
87+
88+
```java
89+
public class InMemoryOrderRepository implements OrderRepository {
90+
private Map<String, Order> database = new HashMap<>();
91+
92+
@Override
93+
public void save(Order order) {
94+
database.put(order.getOrderId(), order);
95+
System.out.println("Order saved: " + order.getOrderId());
96+
}
97+
}
98+
```
99+
4. Entry Point (App)
100+
101+
```java
102+
public class App {
103+
public static void main(String[] args) {
104+
OrderRepository repository = new InMemoryOrderRepository();
105+
OrderService service = new OrderService(repository);
106+
107+
List<String> items = Arrays.asList("Book", "Pen");
108+
Order order = new Order("ORD123", items);
109+
110+
service.placeOrder(order);
111+
}
112+
}
113+
```
114+
115+
- The Domain Layer (Order) has no dependencies.
116+
117+
- The Application Layer (OrderService, OrderRepository) defines operations abstractly without worrying about how data is stored.
118+
119+
- The Infrastructure Layer (InMemoryOrderRepository) provides actual data storage.
120+
121+
- The App Layer wires everything together.
122+
123+
## When to Use Onion Architecture in Java
124+
125+
* Enterprise Applications: Perfect for large systems where business logic must remain stable despite frequent changes in technology or external dependencies.
126+
127+
* Highly Maintainable Systems: Useful when you anticipate frequent feature additions or technology upgrades (e.g., switching from MySQL to MongoDB).
128+
129+
* Test-Driven Development (TDD): Ideal for systems that require extensive unit and integration testing, as the domain can be tested independently.
130+
131+
* Microservices: Helps keep microservices clean, with clear separation between core business rules and communication protocols like REST or gRPC.
132+
133+
## Real-World Applications of Onion Architecture in Java
134+
135+
* Banking and Financial Systems: Where strict control over domain rules and processes is essential, even when interfacing with different databases, APIs, or UIs.
136+
137+
* E-commerce Platforms: Separates critical order, payment, and inventory logic from external services like payment gateways and user interfaces.
138+
139+
* Healthcare Applications: Ensures that patient management, diagnosis, and treatment core logic stays unaffected by changes in reporting tools, hospital systems, or regulatory APIs.
140+
141+
## Benefits and Trade-offs of Onion Architecture
142+
Benefits:
143+
144+
* Separation of Concerns: Clear separation between business logic and technical concerns like databases, UI, or external services.
145+
146+
* High Maintainability: Core business rules can evolve independently of infrastructure or interface changes.
147+
148+
* Enhanced Testability: Inner layers can be unit-tested easily without setting up external dependencies.
149+
150+
* Adaptability: Easier to swap out technologies without touching the core domain.
151+
152+
Trade-offs:
153+
154+
* Initial Complexity: Requires careful design of layers, abstractions, and dependency rules upfront.
155+
156+
* More Boilerplate Code: Interfaces, DTOs, and mappers add to codebase size and complexity.
157+
158+
* Learning Curve: New developers might take longer to understand the structure if they’re not familiar with layered architecture principles.
159+
160+
## Related Architectural Patterns in Java
161+
162+
* [Hexagonal Architecture](https://www.geeksforgeeks.org/hexagonal-architecture-system-design/)
163+
* [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
164+
165+
## References and Credits
166+
167+
* [Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software](https://amzn.to/49NGldq)
168+
* [Onion Architecture by Jeffery Palermo](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/)
169+
* [Onion Architecture - Medium article](https://medium.com/expedia-group-tech/onion-architecture-deed8a554423)
170+
171+

onion-architecture/src/main/Main.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import application.TodoService;
2+
import domain.TodoItem;
3+
import infrastructure.TodoRepositoryImpl;
4+
5+
import java.util.Scanner;
6+
7+
public class Main {
8+
public static void main(String[] args) {
9+
Scanner scanner = new Scanner(System.in);
10+
TodoService service = new TodoService(new TodoRepositoryImpl());
11+
12+
System.out.println("Welcome to the TODO App!");
13+
14+
boolean running = true;
15+
while (running) {
16+
System.out.println("\nWhat would you like to do?");
17+
System.out.println("[a] Add tasks");
18+
System.out.println("[d] Mark tasks as done");
19+
System.out.println("[v] View tasks");
20+
System.out.println("[q] Quit");
21+
System.out.print("> ");
22+
String choice = scanner.nextLine().trim().toLowerCase();
23+
24+
switch (choice) {
25+
case "a":
26+
System.out.println("Enter your tasks (type 'q' to stop adding):");
27+
while (true) {
28+
System.out.print("> ");
29+
String input = scanner.nextLine().trim();
30+
if (input.equalsIgnoreCase("q")) break;
31+
if (!input.isEmpty()) {
32+
service.createTodo(input);
33+
}
34+
}
35+
break;
36+
37+
case "d":
38+
if (service.getTodos().isEmpty()) {
39+
System.out.println("📝 No tasks to mark as done.");
40+
break;
41+
}
42+
System.out.println("\nEnter the ID(s) of tasks to mark as done (comma separated, or 'q' to cancel):");
43+
System.out.print("> ");
44+
String idsInput = scanner.nextLine().trim();
45+
if (!idsInput.equalsIgnoreCase("q")) {
46+
String[] parts = idsInput.split(",");
47+
for (String part : parts) {
48+
try {
49+
int id = Integer.parseInt(part.trim());
50+
service.completeTodo(id);
51+
} catch (NumberFormatException e) {
52+
System.out.println("Invalid ID: " + part);
53+
}
54+
}
55+
}
56+
break;
57+
58+
case "v":
59+
System.out.println("\n📋 Your Todo List:");
60+
if (service.getTodos().isEmpty()) {
61+
System.out.println("No tasks yet!");
62+
} else {
63+
for (TodoItem item : service.getTodos()) {
64+
String status = item.isCompleted() ? "✅ Done" : "❌ Not Done";
65+
System.out.println(item.getId() + ": " + item.getTitle() + " [" + status + "]");
66+
}
67+
}
68+
break;
69+
70+
case "q":
71+
running = false;
72+
break;
73+
74+
default:
75+
System.out.println("Unknown option. Please try again.");
76+
}
77+
}
78+
79+
System.out.println("\nGoodbye! Here's your final Todo List:");
80+
81+
if(service.getTodos().size() == 0)
82+
System.out.println("No tasks left!");
83+
84+
else {
85+
for (TodoItem item : service.getTodos()) {
86+
String status = item.isCompleted() ? "✅ Done" : "❌ Not Done";
87+
System.out.println(item.getId() + ": " + item.getTitle() + " [" + status + "]");
88+
}
89+
}
90+
91+
scanner.close();
92+
}
93+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package application;
2+
3+
import domain.TodoItem;
4+
import domain.TodoRepository;
5+
6+
import java.util.List;
7+
8+
public class TodoService {
9+
private final TodoRepository repository;
10+
private static int idCounter = 1;
11+
public TodoService(TodoRepository repository) {
12+
this.repository = repository;
13+
}
14+
15+
public void createTodo(String title) {
16+
TodoItem item = new TodoItem(idCounter++, title);
17+
repository.add(item);
18+
}
19+
20+
public List<TodoItem> getTodos() {
21+
return repository.getAll();
22+
}
23+
24+
public void completeTodo(int id) {
25+
repository.markAsCompleted(id);
26+
}
27+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package domain;
2+
3+
public class TodoItem {
4+
private int id;
5+
private String title;
6+
private boolean isCompleted;
7+
8+
public TodoItem(int id, String title) {
9+
this.id = id;
10+
this.title = title;
11+
this.isCompleted = false;
12+
}
13+
14+
public int getId() { return id; }
15+
public String getTitle() { return title; }
16+
public boolean isCompleted() { return isCompleted; }
17+
18+
public void markCompleted() {
19+
this.isCompleted = true;
20+
}
21+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package domain;
2+
3+
import java.util.List;
4+
5+
public interface TodoRepository {
6+
void add(TodoItem item);
7+
List<TodoItem> getAll();
8+
void markAsCompleted(int id);
9+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package infrastructure;
2+
3+
import domain.TodoItem;
4+
import domain.TodoRepository;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
9+
public class TodoRepositoryImpl implements TodoRepository {
10+
private final List<TodoItem> todos = new ArrayList<>();
11+
12+
@Override
13+
public void add(TodoItem item) {
14+
todos.add(item);
15+
}
16+
17+
@Override
18+
public List<TodoItem> getAll() {
19+
return new ArrayList<>(todos);
20+
}
21+
22+
@Override
23+
public void markAsCompleted(int id) {
24+
for (TodoItem item : todos) {
25+
if (item.getId() == id) {
26+
item.markCompleted();
27+
break;
28+
}
29+
}
30+
}
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package application;
2+
3+
import application.TodoService;
4+
import domain.TodoItem;
5+
import infrastructure.TodoRepositoryImpl;
6+
7+
import java.util.List;
8+
9+
public class TodoServiceTest {
10+
public static void main(String[] args) {
11+
TodoRepositoryImpl fakeRepo = new TodoRepositoryImpl();
12+
TodoService service = new TodoService(fakeRepo);
13+
14+
service.createTodo("Learn onion architecture");
15+
service.createTodo("Write unit tests");
16+
17+
List<TodoItem> todos = service.getTodos();
18+
19+
assert todos.size() == 2 : "Should have 2 todos";
20+
assert todos.get(0).getTitle().equals("Learn onion architecture");
21+
assert !todos.get(0).isCompleted();
22+
23+
int idToComplete = todos.get(0).getId();
24+
service.completeTodo(idToComplete);
25+
26+
todos = service.getTodos();
27+
assert todos.get(0).isCompleted() : "First item should be completed";
28+
29+
System.out.println("TodoServiceTest passed");
30+
}
31+
}

0 commit comments

Comments
 (0)