Skip to content

Commit 445a562

Browse files
feat: initial commit
1 parent 33d3067 commit 445a562

14 files changed

+2958
-2
lines changed

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.classpath
2+
.project
3+
.settings
4+
target
5+
bin
6+
build
7+
.idea
8+
*.iml
9+
.springBeans
10+
/site/
11+
.venv/

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1-
# tomato-architecture.github.io
2-
Tomato Architecture - A pragmatic approach to software development
1+
# Tomato Architecture
2+
A pragmatic approach to software development.
3+
4+
## Developer Guide
5+
* Install Mkdocs https://www.mkdocs.org/#installation
6+
```shell
7+
$ python -m venv .venv
8+
$ source .venv/bin/activate
9+
$ pip install -r requirements.txt
10+
```
11+
12+
* Run locally: `mkdocs serve` and open http://127.0.0.1:8000/

docs/assets/tomato-arc-logo.png

18.6 KB
Loading
447 KB
Loading

docs/concepts.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Concepts
2+
3+
## 1. Separation of Concerns
4+
//TODO;
5+
6+
7+
## 2. Testability
8+
//TODO;
9+
10+
11+
## 3. Loose Coupling
12+
//TODO;
13+

docs/faq.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Frequently Asked Questions
2+
3+
## What's with the name "Tomato"?
4+
5+
The outer layer of a tomato is strong enough to keep its insides together and yet flexible and fluid inside. That perfectly represents how our software should be designed.
6+
7+
**I am just kidding 😋 You like some made-up words, don't you?**
8+
9+
If you are okay with "Hexagonal," knowing 6 edges has no significance, you should be okay with "Tomato".
10+
After all, we have Onion Architecture, why not Tomato Architecture 🤓
11+
12+
## I don't see anything new in this architecture. Why should I care?
13+
14+
There is nothing new in this architecture. It's just taking the good parts of other architectures and ignoring the cargo-cult programming. 😇

docs/index.md

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
# Tomato Architecture
2+
3+
![tomato-architecture-logo.png](/assets/tomato-arc-logo.png)
4+
5+
**Tomato Architecture** is a pragmatic approach to software architecture following the **Core Principles**
6+
7+
## Core Principles
8+
* Strive to keep things simple instead of overengineering the solution by guessing the requirements for the next decade.
9+
* Do research, evaluate, then pick a technology and embrace it instead of creating abstractions with replaceability in mind.
10+
* Make sure your solution is working as a whole, not just individual units.
11+
* Think what is best for your software over blindly following suggestions by popular people.
12+
13+
## Architecture Diagram
14+
15+
![tomato-architecture.png](/assets/tomato-architecture.png)
16+
17+
## Implementation Guidelines
18+
19+
### 1. Package by feature
20+
A common pattern to organize code into packages is by splitting based on technical layers such as controllers, services, repositories, etc.
21+
If you are building a Microservice which is already focusing on a specific module or business capability, then this approach might be fine.
22+
23+
If you are building a monolith or modular-monolith, then it is strongly recommended to first split by features instead of technical layers.
24+
25+
For more info read: [https://phauer.com/2020/package-by-feature/](https://phauer.com/2020/package-by-feature/)
26+
27+
### 2. "Application Core" for Business Logic
28+
Keep "Application Core" independent of any delivery mechanism (Web, Scheduler Jobs, CLI).
29+
30+
The Application Core should expose APIs that can be invoked from a main() method.
31+
To achieve that, the "Application Core" should not depend on its invocation context.
32+
Which means the "Application Core" should not depend on any HTTP/Web layer libraries.
33+
Similarly, if your Application Core is being used from Scheduled Jobs or CLI, then
34+
any Scheduling logic or CLI command execution logic should never leak into Application Core.
35+
36+
### 3. Separation of Concerns
37+
Separate the business logic execution from input sources (Web Controllers, Message Listeners, Scheduled Jobs, etc).
38+
39+
The input sources such as Web Controllers, Message Listeners, Scheduled Jobs, etc. should be a thin layer extracting the data
40+
from request and delegate the actual business logic execution to "Application Core".
41+
42+
**DON'T DO THIS**
43+
44+
```java
45+
@RestController
46+
class CustomerController {
47+
private final CustomerService customerService;
48+
49+
@PostMapping("/api/customers")
50+
void createCustomer(@RequestBody Customer customer) {
51+
if(customerService.existsByEmail(customer.getEmail())) {
52+
throw new EmailAlreadyInUseException(customer.getEmail());
53+
}
54+
customer.setCreateAt(Instant.now());
55+
customerService.save(customer);
56+
}
57+
}
58+
```
59+
60+
**INSTEAD, DO THIS**
61+
62+
```java
63+
@RestController
64+
class CustomerController {
65+
private final CustomerService customerService;
66+
67+
@PostMapping("/api/customers")
68+
void createCustomer(@RequestBody Customer customer) {
69+
customerService.save(customer);
70+
}
71+
}
72+
73+
@Service
74+
class CustomerService {
75+
private final CustomerRepository customerRepository;
76+
77+
@Transactional
78+
void save(Customer customer) {
79+
if(customerRepository.existsByEmail(customer.getEmail())) {
80+
throw new EmailAlreadyInUseException(customer.getEmail());
81+
}
82+
customer.setCreateAt(Instant.now());
83+
customerRepository.save(customer);
84+
}
85+
}
86+
```
87+
88+
With this approach, whether you try to create a Customer from a REST API call or from a CLI,
89+
all the business logic is centralized in Application Core.
90+
91+
**DON'T DO THIS**
92+
93+
```java
94+
@Component
95+
class OrderProcessingJob {
96+
private final OrderService orderService;
97+
98+
@Scheduled(cron="0 * * * * *")
99+
void run() {
100+
List<Order> orders = orderService.findPendingOrders();
101+
for(Order order : orders) {
102+
this.processOrder(order);
103+
}
104+
}
105+
106+
private void processOrder(Order order) {
107+
...
108+
...
109+
}
110+
}
111+
```
112+
113+
**INSTEAD, DO THIS**
114+
115+
```java
116+
@Component
117+
class OrderProcessingJob {
118+
private final OrderService orderService;
119+
120+
@Scheduled(cron="0 * * * * *")
121+
void run() {
122+
List<Order> orders = orderService.findPendingOrders();
123+
orderService.processOrders(orders);
124+
}
125+
}
126+
127+
@Service
128+
@Transactional
129+
class OrderService {
130+
131+
public void processOrders(List<Order> orders) {
132+
...
133+
...
134+
}
135+
}
136+
```
137+
138+
With this approach, you can decouple order processing logic from scheduler
139+
and can test independently without triggering through Scheduler.
140+
141+
From the Application Core we may talk to database, message brokers or 3rd party web services, etc.
142+
Care must be taken such that business logic executors do not heavily depend on External Service Integrations.
143+
144+
For example, assume you are using Spring Data JPA for persistence, and
145+
from your **CustomerService** you would like to fetch customers using pagination.
146+
147+
**DON'T DO THIS**
148+
149+
```java
150+
@Service
151+
@Transactional
152+
class CustomerService {
153+
private final CustomerRepository customerRepository;
154+
155+
PagedResult<Customer> getCustomers(Integer pageNo) {
156+
Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE, Sort.of("name"));
157+
Page<Customer> cusomersPage = customerRepository.findAll(pageable);
158+
return convertToPagedResult(cusomersPage);
159+
}
160+
}
161+
```
162+
163+
**INSTEAD, DO THIS**
164+
165+
```java
166+
@Service
167+
@Transactional
168+
class CustomerService {
169+
private final CustomerRepository customerRepository;
170+
171+
PagedResult<Customer> getCustomers(Integer pageNo) {
172+
return customerRepository.findAll(pageNo);
173+
}
174+
}
175+
176+
@Repository
177+
class JpaCustomerRepository {
178+
179+
PagedResult<Customer> findAll(Integer pageNo) {
180+
Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE, Sort.of("name"));
181+
return ...;
182+
}
183+
}
184+
```
185+
186+
This way any persistence library changes will only affect the repository layer.
187+
188+
### 4. Domain logic in domain objects
189+
Keep domain logic in domain objects.
190+
191+
**DON'T DO THIS**
192+
193+
```java
194+
195+
class Cart {
196+
List<LineItem> items;
197+
}
198+
199+
@Service
200+
@Transactional
201+
class CartService {
202+
203+
CartDTO getCart(UUID cartId) {
204+
Cart cart = cartRepository.getCart(cartId);
205+
BigDecimal cartTotal = this.calculateCartTotal(cart);
206+
...
207+
}
208+
209+
private BigDecimal calculateCartTotal(Cart cart) {
210+
...
211+
}
212+
}
213+
```
214+
215+
Here `calculateCartTotal()` method contains domain logic purely based on the state of `Cart` object.
216+
So, it should be part of the domain object `Cart`.
217+
218+
**INSTEAD, DO THIS**
219+
220+
```java
221+
222+
class Cart {
223+
List<LineItem> items;
224+
225+
public BigDecimal getTotal() {
226+
...
227+
}
228+
}
229+
230+
@Service
231+
@Transactional
232+
class CartService {
233+
234+
CartDTO getCart(UUID cartId) {
235+
Cart cart = cartRepository.getCart(cartId);
236+
BigDecimal cartTotal = cart.getTotal();
237+
...
238+
}
239+
}
240+
```
241+
242+
### 5. No unnecessary interfaces
243+
Don't create interfaces with the hope that someday we might add another implementation for this interface.
244+
If that day ever comes, then with the powerful IDEs we have now, it is just a matter of extracting the interface in a couple of keystrokes.
245+
246+
If the reason for creating an interface is for testing with Mock implementation,
247+
we have mocking libraries like Mockito which are capable of mocking classes without implementing interfaces.
248+
249+
So, unless there is a good reason, don't create interfaces.
250+
251+
### 6. Embrace framework's power and flexibility
252+
Usually, the libraries and frameworks are created to address the common requirements that are required for a majority of the applications.
253+
So, when you choose a library/framework to build your application faster, then you should embrace it.
254+
255+
Instead of leveraging the power and flexibility offered by the selected framework,
256+
creating an indirection or abstraction on top of the selected framework with the hope
257+
that someday you might switch the framework to a different one is usually a very bad idea.
258+
259+
For example, Spring Framework provides declarative support for handling database transactions, caching, method-level security etc.
260+
Introducing our own similar annotations and re-implementing the same features support
261+
by delegating the actual handling to the framework is unnecessary.
262+
263+
Instead, it's better to either directly use the framework's annotations or compose the annotation with additional semantics if needed.
264+
265+
```java
266+
@Target(ElementType.TYPE)
267+
@Retention(RetentionPolicy.RUNTIME)
268+
@Documented
269+
@Transactional
270+
public @interface UseCase {
271+
@AliasFor(
272+
annotation = Transactional.class
273+
)
274+
Propagation propagation() default Propagation.REQUIRED;
275+
}
276+
```
277+
278+
### 7. Test not only units, but whole features
279+
280+
We should definitely write unit tests to test the units (business logic), by mocking external dependencies if required.
281+
But it is more important to verify whether the whole feature is working properly or not.
282+
283+
Even if our unit tests are running in milliseconds, can we go to production with confidence? Of course not.
284+
We should verify the whole feature is working or not by testing with the actual external dependencies such as database or message brokers.
285+
That gives us more confidence.
286+
287+
I wonder if this whole idea of "We should have core domain completely independent of external dependencies" philosophy
288+
came from the time when testing with real dependencies is very challenging or not possible at all.
289+
290+
Luckily, we have better technology now (ex: [Testcontainers](https://testcontainers.com/)) for testing with real dependencies.
291+
Testing with real dependencies might take slightly more time, but compared to the benefits, that's a negligible cost.

docs/spring-boot-impl.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Tomato Architecture - Spring Boot
2+
3+
A reference implementation of Tomato using Spring Boot.
4+
5+
[https://github.com/sivaprasadreddy/tomato-architecture-spring-boot-demo](https://github.com/sivaprasadreddy/tomato-architecture-spring-boot-demo)
6+

0 commit comments

Comments
 (0)