Modular backend and transaction design
Where atomicity is decided, and how the layers divide the work.
Documented design
- 65 domain modules, 62 controllers, 93 services and 56 repositories. Controllers are thin: none injects a repository or the data source.
- Services open transactions with DataSource.transaction and pass the EntityManager to repository methods, so multi-table writes commit or roll back together (about 100 transaction call sites across 67 services).
- Repositories only build queries: reads use the shared manager, writes use the transaction's manager, and domain errors are raised in services.
- Row locks (findForUpdate) guard read-modify-write steps, and sequential codes are generated inside the transaction so a failed insert rolls the counter back.
- Connection-level statement and idle-in-transaction timeouts are set in the shared data-source configuration.
Design rationale
- Putting the boundary in the service lets one use case map to one transaction, however many repositories it touches.
await dataSource.transaction(async (manager) => { await repositoryA.write(manager, changeA); await repositoryB.write(manager, changeB);}); // both writes commit or roll back together