Some background
Having built numerous event and domain driven systems in Laravel/PHP using techniques developed in no small part from the excellent courses provided by Spatie (Laravel Beyond CRUD and Event Sourcing in Laravel).
I wondered how other development ecosystems were handling such concepts and last year I found myself diving deep into “Architecture Patterns with Python: Enabling Test-Driven Development, Domain-Driven Design, and Event-Driven Microservices”, a book I now consider compulsory reading for any software developer.
I decided to spend some time giving Python some love and really hammer down some of the methodologies presented in the book in the language they were originally intended. The result of which was my nomad scraper open source web scraping application I released some months back. The scraper was an off-shoot of a much larger project I currently have in the works (more on that another time).
Fast forward a few months and many completed microservices later I realised that I was now hooked on this architecture and had re-written it in TypeScript numerous times. Each time somewhat updating and improving the flow to suit what I consider best practice and logical separation of concerns and further refining it to ensure adherence to my article on SOLID principles.
Development
Eventually my opinionated implementation of this architecture began to take on a life of its own and progressed into what is probably considered a very-mini-framework, so I decided to package it up and release it on GitHub. I did not set out to build a framework of any kind. I was simply tired of stitching together the same moving parts across services: commands, handlers, events, units of work, and the domain. The pattern was always there, even when unacknowledged. Eventually it became easier to embrace it outright, set some standards and save myself considerable time in the future.
There are two repositories for anyone to use, both repositories revolve around a single idea: everything that happens in the system should be the result of an explicit message on an internal message bus. A command says “do this”. An event says “this happened”. One command performs one logical operation which in turn can emit one or more events in a specified order. Events handlers only ever push new commands to the message bus and thus the cycle continues until all is processed. All commands adopt the unit of work pattern for entirely atomic data handling.
That alone brings more clarity than service classes calling each other in unpredictable ways. There is no magic, no auto-dispatching. A command is handled by exactly one handler. An event may lead to new commands. Nothing jumps the queue.
The architecture is still in its infancy, I have intentions to release considerable updates to these repositories as they develop.
The Framework: driven-micro-framework
https://github.com/OffRoadCodeMode/driven-micro-framework
The underlying package provides the contracts. Interfaces for commands, events, handlers, repositories, units of work, and the internal bus that moves them. There is no HTTP layer here, no database, no logging. It exists purely to enforce the discipline of message flow and prevents you needing to write that system yourself (or myself).
It also enforces a strict divide: command handlers are allowed to change state (via repositories), event handlers are not. Event handlers exist only to decide which command should follow. That separation keeps domain logic anchored in intent, not reaction.
Dependency injection via definitive interfaces ties it together. Every handler is resolved by the container, which means you never have to new up a repository halfway through a use case. If you want to replace DynamoDB with Postgres, you bind a different implementation, the domain never notices.
The Domain Template: driven-micro-domain
https://github.com/OffRoadCodeMode/driven-micro-domain
This repository shows what it looks like in practice. Domain on one side, infrastructure on the other. An app bootstrap that wires concrete implementations needed by handlers to the bus. Example endpoints that translate HTTP requests or Lambda events into commands. A DynamoDB adapter comes out of the box that meets the repository interface. You create your own concrete implementations of the various framework provided interfaces in here, more ready made adapter implementations will be added to this repo over time.
It also includes a full test setup. Because everything talks through the bus, you can run entire workflows in memory. No HTTP server required.
More features to come
True Event Sourcing
Event sourcing is not fully implemented yet, but the architecture is deliberately structured so that it can be. Commands and events already form the audit trail. Introducing persistence for events and allowing replay is an additive step that is planned very soon.
Multithreaded Commands and bus
I intend to add the ability to run commands on separate worker threads and return events to the main thread via shared memory. While most of my services end up on Lambda and concurrency is primarily achieved as standard there are use cases where wasted vCPU could be better utilised while not affecting overall account concurrency limits. Plus it would open options into how you split your workload and on what kind of hardware.