Skip to content

Architecture — usrv-finances

Architecture reference for the microservice. For domain flows see docs/.


Hexagonal Architecture

The project strictly follows hexagonal architecture with 4 layers:

LayerDirectoryResponsibility
Domainsrc/service/Pure business logic, no framework dependencies. All methods return Observable<T>
Applicationsrc/handler/AWS Lambda entry points. Wrap services with Middy middleware. Convert ObservablePromise via lastValueFrom()
Portssrc/repository/Interfaces (contracts). I prefix. Define contracts between domain and infrastructure
Infrastructuresrc/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/await in the service layer

Gateway

  • Wrap AWS SDK clients
  • Return Observable<T> using from() 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)PurposeKey GSIs
AccountsTableFinancial accountsentity_id-index, account_number-index, status-index
JournalsTableAccounting journalsentity_id-index, process_id-index, entity_id-created_at-index, expected_reserve_release_date-index
LedgerEntriesTableLedger entriesentity_id-index, process_id-index, journal_id-index
FeeRulesTableFee calculation rulesfee_rule_key-index
BulkReprocessTableBulk reprocess jobsstatus-created_at-index, business_id-created_at-index
BalanceSnapshotsTableDaily balance snapshots
ConfigTablePer-business configurations
IdempotencyTableEvent deduplicationTTL enabled

Global tables replicated to us-west-2.

MongoDB

  • Connection via MongoGateway with 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
          → transactionOrchestratorHandler

SQS Queues

QueueTypeDLQMax retriesUsage
TransactionEventsQueueFIFOTransactionEventsDLQ5EventBridge events
AccountUpdateQueueFIFOAccountUpdateDLQ160 (intentional)Balance updates
SettlementJournalsQueueStandardSettlementJournalsDLQ5Journal 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

StageStrategyLog retentionUsage
devAllAtOnce1 dayDevelopment
stageAllAtOnce7 daysQA/Testing
pdnLinear10PercentEvery2Minutes10 yearsProduction
  • Custom domain: basePath: /finances via 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 .ts files
  • commitlint: Conventional commits (feat, fix, refactor, docs, test, chore)
  • Commit fails if linting or tests fail

Key environment variables

VariableUsage
USRV_STAGEDeployment stage (dev/stage/pdn)
USRV_NAMEService name (for CloudWatch metrics)
LOGS_ENABLEDControls detailed logging in handlers
BULK_REPROCESS_STATE_MACHINE_ARNState machine ARN
FINANCES_FILES_BUCKETS3 bucket for batches and reports
SQS_ACC_UPDATE_QUEUE_URLAccountUpdateQueue URL
SQS_SETTLEMENT_JOURNALS_QUEUE_URLSettlementJournalsQueue URL
MONGO_CONFIGMongoDB connection config (JSON)
MONGO_ROL_ARNIAM role ARN for MongoDB

Naming Conventions

ElementConventionExample
InterfacesPascalCase with I prefixIAccountService, IJournal
ClassesPascalCaseAccountService, DynamoGateway
MethodscamelCasefeesCalculator(), accountingPreparer()
Private propscamelCase with __logger, _dynamoGateway
RxJS Subjects$ suffixevents$

ESLint limits: max 200 lines per function · max 10 parameters · no any · explicit return types · no unused variables (prefix _ if intentional)

Vecnet — Build Spec v0.2 · Obsidian Terminal