Clean/Hexagonal/Onion ... Architecture
REX
Why ?
1# hard to find business rules
2# hard to reuse logic...
3# hard to update/find out an existing behaviour
4# hard to test
Development Cycle time to long
+ Hard to maintain
How we do it ?
1 - Looking at article, video(s), book(s)
2 - First draft with standalone mode
Clean Architecture Book
3 - Structure choice (how to reuse)
4 - Testing with inMemory
5 - Modularization
... - ....
Domain
akka business
Adapters
Configuration
Benefits
Demo Ready
Delay technical choice
and having a working software quickly
by using an in memory source
API
(not ready)
wich DB ?
standalone app
🤔
🤗 🚀
Switching deps
AWS stepfunction
Job AI API
UseCase
Source
PinoJs
NestjsLogger
Logger
Source
Another logger without impacting all the code
New source without changing the use case
Easy to unit test
sql adapter
In memory
UseCase
Source
Unit Test
- Test by use case
- No mock in test
- Ability to test effect (ex: insertion, read...)
- No Implementation details coupling
- ....
Readable Structure
Package representation
E2E Ready
Fast E2E on CI => available on every commit
First increment to E2E test
Using E2E locally and on CI without DB !
Second increment to E2E test
Using E2E for release by just switch deps
Modulare software
Domain A
Domain B
Domain C
- Reuse of services (!= use cases)
- Independant or sharing dependency (with nest)
- Runtime "private package" emulated
Pitfalls
Abstraction
Hopefully we had a refactorable code !
#thxCleanArchi
#thxTestByUseCase
3 months to remove the bad abstraction...
Abstraction
duplication is far cheaper than the wrong abstraction
prefer duplication over the wrong abstraction
What we learn ?
Our story in video :
Use-case usage
By reusing use-cases we had:
- understanding issues
- Testing issues
- Perfomance issues
- A UseCase should the most independant it can be
- A UseCase is not a service, it's the entry point to the domain layer and it might orchestrate several service...
- Avoid useCase reuse if you can
Source/Port
By Making too specific "queries" you might put business on it
Ex:
SELECT u.name as universe_name,
MAX(case when (c.scheduled_day >= $date) then NULL ELSE c.scheduled_day end) as last_shoot_date,
MIN(case when (c.scheduled_day <= $date) then NULL ELSE c.scheduled_day end) as next_shoot_date
FROM ad_universe u
LEFT JOIN ad_campaign_universe cu ON u.id = cu.universe_id
LEFT JOIN ad_campaign c ON c.id = cu.campaign_id OR c.domain = u.domain AND c.all_universes
WHERE u.domain = $domain
AND c.enabled
AND u.enabled
GROUP BY universe_name
By Making too CRUD "queries" you might:
- - Introduce coupling to attribute you do not need
- - have perfomance issue (ex: blocking constraint with transaction)
Ex: update(user) but the use-case just want to update one field here...
Transaction
We had perfomance issues by using:
- Business transaction
- Too strict transaction mode
So... Think twice before adding a transaction...
Think twice before adding a transaction !!!
Keep the transaction in the adapter side or be careful
NestJs, DbLib, ApiLib
Avoid NestJs or over tools (lib) interference with you business because it will increase the complexity
One update can make you change a lot of code, be careful
(so avoid it)
Use some trick like Dependency Inversion to do not depend on them...
Dependencies
We have some cyclique dependencies issues between :
- Files (import)
- Classes
This kind of pb is hard to debug !
We have avoided it by :
- cyclic import detection
"circular-deps:find": "npx madge --circular --extensions ts ./src/"
- using only required dependency (ies?)
- Avoiding import between domain context
- Avoiding unused export :
"ts-prune": "npx ts-prune",
no business responsibility
serviceA()
doExternalCall()
function serviceA(n) {
doExternalCall()
}
function doExternalCall(n) {
// complex call orchestration
// complex sql Request
...
}
behaviour tree
}
PB business responsibility delegated to the integration part
Integration part
business part ?
}
PB complex integration test needed
Moving business responsability
serviceA()
doExternalCall()
function serviceA(n) {
// business logic
doExternalCall()
}
function doExternalCall() {
// simple sql or api request
}
behaviour tree
}
simple integration test
Integration part
business part
}
No Business responsibility
delegated
Moving business responsability
serviceA()
doExternalCall()
function serviceA(n) {
// business logic
doCall()
}
function doExternalCall() {
// simple sql or api request
// here external call
}
behaviour tree
simple unit test
Integration part
business part
}
In memory state
function doInMemoryCall() {
// simple logic
}
deck
By orangefire
deck
- 38