Appearance
Architecture — usrv-finances
Architecture reference for the microservice. For domain flows see docs/.
Hexagonal Architecture
The project strictly follows hexagonal architecture with 4 layers:
| Layer | Directory | Responsibility |
|---|---|---|
| Domain | src/service/ | Pure business logic, no framework dependencies. All methods return Observable<T> |
| Application | src/handler/ | AWS Lambda entry points. Wrap services with Middy middleware. Convert Observable → Promise via lastValueFrom() |
| Ports | src/repository/ | Interfaces (contracts). I prefix. Define contracts between domain and infrastructure |
| Infrastructure | src/gateway/ | Adapters to external services (DynamoDB, MongoDB, SQS, S3, Lambda, Firehose) |
Dependency Injection (InversifyJS)
- All bindings in
src/infrastructure/container.ts - Type symbols in
src/constant/types.ts - Handlers resolve dependencies at runtime:
container.get<IService>(TYPES.Service) - AWS SDK clients registered as singletons
Never instantiate services or gateways directly. Always use the container.
Code Patterns
Handler
typescript
const fnHandler = async (event: IApiGatewayEvent<TRequest>): Promise<TResponse> => {
const service = container.get<IService>(TYPES.Service);
return await lastValueFrom(service.method(event));
};
export const handler = middy(fnHandler)
.use(warmupMiddleware())
.use(inputOutputLoggerMiddleware(...))
.use(httpEventNormalizerMiddleware())
.use(httpJsonBodyParserMiddleware())
.use(REQUEST_VALIDATION_MIDDLEWARE({ body: "schema_name" }))
.use(httpErrorHandlerMiddleware());Service
- Dependencies injected via constructor with
@inject - Methods return
Observable<T> - Operators:
map,switchMap,tap,catchError,forkJoin - Never
async/awaitin the service layer
Gateway
- Wrap AWS SDK clients
- Return
Observable<T>usingfrom()for Promises - Handle error transformation and logging
Storage
DynamoDB
All tables use PAY_PER_REQUEST billing and have DynamoDB Streams enabled.
Table (constant in Tables.ts) | Purpose | Key GSIs |
|---|---|---|
AccountsTable | Financial accounts | entity_id-index, account_number-index, status-index |
JournalsTable | Accounting journals | entity_id-index, process_id-index, entity_id-created_at-index, expected_reserve_release_date-index |
LedgerEntriesTable | Ledger entries | entity_id-index, process_id-index, journal_id-index |
FeeRulesTable | Fee calculation rules | fee_rule_key-index |
BulkReprocessTable | Bulk reprocess jobs | status-created_at-index, business_id-created_at-index |
BalanceSnapshotsTable | Daily balance snapshots | — |
ConfigTable | Per-business configurations | — |
IdempotencyTable | Event deduplication | TTL enabled |
Global tables replicated to us-west-2.
MongoDB
- Connection via
MongoGatewaywith IAM role assumption - Environment variables:
MONGO_CONFIG,MONGO_ROL_ARN - Connection middleware:
MongoConnectionMiddleware(automatic pooling) - DB and collection names in
src/constant/MongoResources.ts
Main collections: financesJournals, financesLedgerEntries, financesAccounts, financesFeeRules, financesConfig, financesBalanceSnapshots
Source transaction collections (read during bulk): see src/utils/acquirerCollectionMap.ts
PostgreSQL
- Connection via AWS RDS Proxy with IAM authentication
- Token auto-generated on each connection
- Variables:
PG_DB_PROXY_ENDPOINT,PG_DB_USER,PG_DB_NAME - Middleware:
PGConnectionMiddleware
Event-Driven Architecture
EventBridge → SQS
usrv-data-sync-sls-{stage}-transactions-bus
Filters: PAYMENTS, APMS, WITHDRAWALS, DISPUTES
→ TransactionEventsQueue (FIFO)
MessageGroupId = businessId ← guarantees per-business ordering
→ transactionOrchestratorHandlerSQS Queues
| Queue | Type | DLQ | Max retries | Usage |
|---|---|---|---|---|
TransactionEventsQueue | FIFO | TransactionEventsDLQ | 5 | EventBridge events |
AccountUpdateQueue | FIFO | AccountUpdateDLQ | 160 (intentional) | Balance updates |
SettlementJournalsQueue | Standard | SettlementJournalsDLQ | 5 | Journal updates with settlement_id |
All queues have content-based deduplication enabled.
accountUpdaterHandler has maximumConcurrency: 10 on its SQS event source mapping. This limits concurrent Lambda invocations consuming from AccountUpdateQueue, preventing DynamoDB throttling during bulk operations.
Step Functions
STANDARD state machine bulk-reprocess-{stage} for Bulk Reprocess / Initial Load. See full documentation in docs/bulk/bulk-reprocess.md (Tonder source — not included in this package).
Deployment and Environments
| Stage | Strategy | Log retention | Usage |
|---|---|---|---|
dev | AllAtOnce | 1 day | Development |
stage | AllAtOnce | 7 days | QA/Testing |
pdn | Linear10PercentEvery2Minutes | 10 years | Production |
- Custom domain:
basePath: /financesvia API Gateway - Canary deployments: Lambda aliases with CodeDeploy in production
- Tagging: resources tagged for cost tracking
Pre-commit hooks (Husky)
lint-staged: ESLint + Prettier on staged.tsfilescommitlint: Conventional commits (feat,fix,refactor,docs,test,chore)- Commit fails if linting or tests fail
Key environment variables
| Variable | Usage |
|---|---|
USRV_STAGE | Deployment stage (dev/stage/pdn) |
USRV_NAME | Service name (for CloudWatch metrics) |
LOGS_ENABLED | Controls detailed logging in handlers |
BULK_REPROCESS_STATE_MACHINE_ARN | State machine ARN |
FINANCES_FILES_BUCKET | S3 bucket for batches and reports |
SQS_ACC_UPDATE_QUEUE_URL | AccountUpdateQueue URL |
SQS_SETTLEMENT_JOURNALS_QUEUE_URL | SettlementJournalsQueue URL |
MONGO_CONFIG | MongoDB connection config (JSON) |
MONGO_ROL_ARN | IAM role ARN for MongoDB |
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Interfaces | PascalCase with I prefix | IAccountService, IJournal |
| Classes | PascalCase | AccountService, DynamoGateway |
| Methods | camelCase | feesCalculator(), accountingPreparer() |
| Private props | camelCase with _ | _logger, _dynamoGateway |
| RxJS Subjects | $ suffix | events$ |
ESLint limits: max 200 lines per function · max 10 parameters · no any · explicit return types · no unused variables (prefix _ if intentional)