How to develop my first feature ?
What are specification ?
Are acceptance criteria clear ?
Is the tech part clear for the team ?
No : Tech refinement
No : Team refinement
No : Product refinement
How to develop my first feature ?
SETUP your backend environment
git clone git@github.com:sencrop/sencrop-bali-api.git
npm ci
npm run prisma:generate
npx env-cmd -f .env.preproduction npm run start
wait-on http://localhost:4000/health --timeout 60000
How to develop my first feature ?
Where do i put my egg in nest ?
Where do i implement my feature ?
1 - first in "app" : to implement your sencrop app related use-cases
2 - then in "infrastructure" : to connect with the external world
How to develop my first feature ?
How to start my first implementation ?
1 - Specify your use-case :
authorization.use-cases.spec.ts
authorization.use-cases.ts
session.in-memory.ts
Make a PR 🚀
2 - Connect it a real world source
export interface SessionSource {
findOneByUserId(userId: number): Promise<Session | undefined>;
}
authorization.use-cases.ts
session.source.ts
session.repository.ts
Make a PR 🚀
3 - Make it available for an external usage
a - Export
b - Import
c - Use
authorization.module.ts
rest-api.module.ts
admin.guard.ts
Make a PR 🚀
Some key concepts
Dependency injection, or DI
Dependencies are services or objects that a class needs to perform its function. Dependency injection, or DI, is a design pattern in which a class requests dependencies from external sources rather than creating them.
https://angular.io/guide/dependency-injection
export class AuthorizationUseCases {
private readonly sessionSource: SessionSource,
private readonly fieldSource: FieldSource,
private readonly loggerSource: LoggerSource
constructor() {
this.sessionSource = new SessionRepository()
this.fieldSource = new FieldRepository()
this.loggerSource = new LoggerProvider()
}
Knows about dependencies construction
Make a new instance of each dependencies
on AuthorizationUseCases creation
constructor injection
dependencies
const authorizationUseCase = new AuthorizationUseCase(
new SessionRepository(),
new FieldRepository(),
new LoggerProvider()
)
do not know about dependencies construction
Take the provided instance without of taking care of how they have been created
Dependency Inversion, or DI
https://en.wikipedia.org/wiki/Dependency_inversion_principle
export class ForecastsUseCases {
constructor(
private readonly meteoblueClient: MeteoblueClient
) {
}
public getHourlyForecasts(location: {lat: number, lon: number}): Promise<ExpectedResult> {
/*
* A lot of business logic here
*
*/
const result = await this.forecasts.get(`/multimodel-1h`, {
params: {
lat,
lon,
},
});
const forecasts = this.toForecasts(result.data);
/*
*
*
* A lot of business logic here
*
*/
return expectedResult
}
specific to meteoblue...
export class ForecastsUseCases {
constructor(
private readonly forecastsSource: ForecastsSource
) {}
public getHourlyForecasts(location: {lat: number, lon: number}): Promise<ExpectedResult> {
// A lot of business logic here
const forecasts = forecastsSource.get(location)
return expectedResult
}
export class forecastsFromMeteoBlue implements ForecastsSource {
constructor(
private readonly meteoblueClient: MeteoblueClient
) {}
public getHourly(location: {lat: number, lon: number}): Promise<HourlyForecasts> {
//A lot of business logic here
const result = await this.meteoblueClient.get(`/multimodel-1h`, {
params: { lat, lon },
});
return this.toForecasts(result.data);
}
interface ForecastsSource {
getHourlyForecasts(location: {lat: number, lon: number}): Promise<HourlyForecasts>
}
Easy to change
Switchable
Some Nest framework concept used
How nest could help me to connect with the outside world ?
Decorators : Controller / Guard / Route
@nestjs/common
How nest could to connect all my stuff together ?
new Auth0Controller(new UserMigrationUseCases())
Dependency injection
async function boostrap() {
NestFactory.create(AppModule)
}
main.ts
app.module.ts / Root module
rest-api.module.ts
authorization.module.ts
@nestjs/common
Module : create scope/isolation and manage dependencies (reuse etc...)
How do i apply same logic accross all requests ?
Guard : CanActivate implementation
How to make a dependency inversion ?
Needed dependencies
Say here how to build your dependency
Give your dependency to nest DI (dependency injection)
How to manage errors ?
1 - Manage business error in the use-case not in the adapter
Ex : findUser(id: string): Promise<User | undefined>
not : findUser(id: string): Promise<User>
2 - Always give the initial root cause (use cause params)
3 - Always throw error after a try catch or log an error
4 - Always bind it to a HTTP Error (if not -> 500)
5 - Observe your error (trace, logs...)
Errors best practices
Business Errors
Some naming and definitions
my-use-case.use-case.ts
my-source.source.ts
my-controller.controller.ts
my-module.module.ts
my-guard.guard.ts
my-service.service.ts
my-client.client.ts
my-mapper.mapper.ts
my-model.model.ts
my-source.source.shared.ts
my-unit-test.test.ts
my-unit-test.spec.ts
my-integration-test.itest.ts
my-type.type.ts
my-adapter.[adapterName].ts
my-db-adapter.repository.ts
my-adapter.[adapterName].ts
my-local-adapter.local.ts
my-in-memory-adapter.in-memory.ts
my-type.decorator.ts
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
Delay technical choice
and having a working software quickly
by using an in memory source
API
(not ready)
wich DB ?
standalone app
🤔
🤗 🚀
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
sql adapter
In memory
UseCase
Source
Unit Test
Package representation
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
Domain A
Domain B
Domain C
Hopefully we had a refactorable code !
#thxCleanArchi
#thxTestByUseCase
3 months to remove the bad abstraction...
duplication is far cheaper than the wrong abstraction
prefer duplication over the wrong abstraction
What we learn ?
Our story in video :
By reusing use-cases we had:
- understanding issues
- Testing issues
- Perfomance issues
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:
Ex: update(user) but the use-case just want to update one field here...
We had perfomance issues by using:
So... Think twice before adding a transaction...
Think twice before adding a transaction !!!
Keep the transaction in the adapter side or be careful
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...
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",
serviceA()
doExternalCall()
function serviceA(n) {
doExternalCall()
}
function doExternalCall(n) {
// complex call orchestration
// complex sql Request
...
}
PB business responsibility delegated to the integration part
Integration part
business part ?
PB complex integration test needed
serviceA()
doExternalCall()
function serviceA(n) {
// business logic
doExternalCall()
}
function doExternalCall() {
// simple sql or api request
}
simple integration test
Integration part
business part
No Business responsibility
delegated
serviceA()
doExternalCall()
function serviceA(n) {
// business logic
doCall()
}
function doExternalCall() {
// simple sql or api request
// here external call
}
simple unit test
Integration part
business part
In memory state
function doInMemoryCall() {
// simple logic
}