Skip to content

s4nt14go/white-label

Repository files navigation

Serverless Domain-Driven Design (DDD) with unit tests

Domain Events are a fundamental building block in DDD, if you want to indicate an event that is significant to your app, raise this event and let other modules of your app subscribe and react to it.

This project exemplifies a CreateUser use case and how we can trigger an event, signaling we have a new user onboard.

Domain events (e.g. UserCreatedEvent) are dispatched after the aggregates (User) changes are persisted in the database. We can subscribe to it from the same module (SomeWork) or a different one (StoreEvent, NotifySlackChannel, CreateAccount).

Communication in the same module (as the case of SomeWork) is given as an example but using domain events for intra-module communication may involve adding an indirection that doesn't add value and a direct/explicit flow is more convenient.

The lambda entry point for CreateUser use case is src/modules/users/useCases/createUser/index.ts, there we:

  • Create CreateUser controller
  • In CreateUser.constructor we register UserCreatedEvent to an intermediate lambda DistributeDomainEvents that will invoke all its subscribers (StoreEvent, CreateAccount, NotifySlackChannel and SomeWork lambdas).

This is the transaction tracing from Lumigo:


graph


Timelines

If it's been some time since last request, we get cold starts and the execution of createUser takes ~1,2s, while all the invoked lambdas (distributeDomainEvents, createAccount, notifySlackChannel, storeEvent and someWrok) take an extra ~2,1s, so the whole distributed transaction takes ~3,3s:


timeline1


If we repeat a request in the next 5m, we don't have cold starts, createUser takes 240ms, all the invoked lambdas an extra ~280ms, so the whole distributed transaction takes ~520ms:


timeline2


Event modeling


event modeling


Decorators

For cross-cutting concerns these decorators are used:

Tests

Unit tests (with faked repos):

Integration tests (real repos):

E2E tests:

Class hierarchy

Blue solid lines are extends, while green dashed ones are implements.


graph

graph

graph

graph

graph

graph

graph

graph

graph


SQL diagram

graph

Stack

I've used SST Serverless Stack as it allows debugging lambda code locally while being invoked remotely by resources in AWS.

Credits

I started this project using Khalil Stemmler's white-label users module and applied some concepts based on Vladimir Khorikov courses where he tackles DDD in a great way.

I also added modules accounts, audit, notifications, decorators for cross-cutting concerns to support SQL transactions and DB retry, refactored from REST to GraphQL API, turned it into serverless and wrote a bunch of tests.

Instructions

Use same Node 16 version as in the pipeline, using nvm you can:

# set Node 16 in current terminal
nvm use 16
# set Node 16 as default (new terminals will use 16)
nvm alias default 16

Install and run tests:

npm ci
npm test

It may take a few minutes to run all unit tests. For new projects, I recommend using vitest, which is much faster.