Because every Wedding RSVP website needs to follow DDD, CQRS, Hexagonal Architecture, Event Sourcing, and be deployed on Lambda.
Because every Wedding RSVP website needs to follow DDD, CQRS, Hexagonal Architecture, Event Sourcing, and be deployed on Lambda.
🌎 Website | 📷 Gallery | 🏗️ Infrastructure
This application (and associated infrastructure) documents an approach to building complex systems which require the benefits that DDD, Hexagonal Architecture and Event Sourcing provide. On-top of this it shows how such an application can be combined with Terraform and deployed in a Serverless manor.
Using PHP and the Symfony framework it highlights how such an approach can be laid out, coupled with a sufficient testing strategy and local development environment. Some topics and features covered within this application are:
Prerequisite: ensure you have Docker installed on your local machine.
make start
make open-web
make can-release
All available actions within the local development environment are available (and documented) within the Makefile by running make help
.
The application follows CQRS for interaction between the Ui and Application, Hexagonal Architecture to decouple the Infrastructural concerns, and DDD/Event Sourcing to model the Domain.
Following Hexagonal Architecture, the layers have been defined like so:
Based on the above layers, we employ three distinct message buses (Command, Aggregate Event and Domain Event), modeling the Aggregates using Event Sourcing. The following diagram highlights how these three buses interact during a typical Command/Query lifecycle response.
There are two Aggregates within the Domain (FoodChoice and Invite), the Aggregate Event flow for both goes as follows:
This diagram is automatically generated based on the current implementation, using testable Event snapshots at the Command level.
Application-level Commands which are available for the Ui to interact with the Domain are presented below:
Along with the Command and Command Handlers, this also deptics the associated Domain Events which are emitted.
The testing strategy employed within this application helps aid in following a Test Pyramid, favouring testing behaviour over implementation. In doing this we exercise most of our behavioral assertions at the Application layer, testing the public API provided by the Commands and Query services. This provides us with a clear description of the intended application's behaviour, whilst reducing the brittleness of the given tests as only public contracts are used.
Testing has been broken up into a similar Hexagonal Architecture layered representation as the system itself, like so:
Domain
Low-level domain testing which is heavily coupled to the current implementation. This is used in cases where you wish to have a higher-level of confidence of a given implementation which can not be easier asserted at the Application level.
Application
Unit tests (ala Unit of behaviour) which use the public API exposed by the Commands and Query services to assert correctness. These are isolated from any infrastructural concerns (via test doubles) and exercise the core business logic/behaviour that the application provides. This level provides us with the greatest balance between asserting that the current implementation achieves the desired behaviour, whilst not being over-coupled to the implementation causing the test to become brittle. Depending only on the public API within these tests allows us to refactor the underlying Domain implementation going forward whilst keeping the tests intact. As such, it is desired to have the most amount of testing at this level.
Infrastructure
Contractual tests to assert that the given adaptor implementation completes the required ports responsibility; communicating with external infrastructure (such as a database) in isolation to achieve this.
Ui
Full-system tests which exercises the entire system by-way of common use-cases. This provides us with confidence at the highest-level that the application is achieving the desired behaviour.
The application uses the following linting tools to maintain the desired code quality and application correctness.
app/psalm.xml
).app/.php-cs-fixer.php
).depfile.yml
).app/package.json
).These tools can be run locally using make lint
, returning a non-zero status code upon failure.
This process is also completed during a make can-release
invocation.
The application is hosted on AWS Lambda with transient infrastructure (which change based on each deployment) being provisioned using the Serverless Framework. Resources managed at this level include Lambda functions, API-gateways and SQS event integrations. Foundational infrastructural concerns (such as networking, databases, queues etc.) are provisioned using Terraform and can be found in the related repository.
Sharing between Terraform and Serverless Framework is unidirectional, with the application resources that Serverless Framework creates being built upon the foundation that Terraform resources provision. Parameters, secrets and shared resources which are controlled by Terraform are accessible to this application via SSM parameters and Secrets Manager secrets; providing clear responsibility separation.
The application consists of an Event Store which persist aggregate events produced by invoked Commands. It also includes Projections which persist materialised views of these aggregate events, accessible via Query services. These two responsibilities do not require a shared data-store, and can manage their own state as best they see fit.
To highlight this, I have built several persistence implementations of both the Event Store and Projections. This exercises the importance of good abstraction, and the benefits of layering your application using Hexagonal Architecture (Ports and Adaptors). Although only one is used within the production setting (Postgres), as a local demonstration it is interesting to see the differences.
Using the provided Event Store port interface, there are the following implementations:
Postgres
DynamoDB
Future developments that I wish to consider, are to instead store the events within S3 for persistence (possibly via DynamoDB streams for durability), and then query for these using Athena when streaming operations are required. In doing this we would move away from the hot key that has been introduced in its current form.
EventStoreDB
With the HTTP and Atom communication protocols being deprecated, work to replace this with a gRPC client written in PHP would need to be carried out before putting such an implementation in production.