Domain Driven Design is about focusing on the business logic first, everything around that are just steps to achieve it. Yet those steps are important, as they help to create environment in which full business focus is possible. We can’t really give full attention to the business problems if our system:
- Fails randomly when some integration is down.
- We get inconsistency in data because something was not sent or received correctly.
- We spend hours maintaining non-business related code like configurations, integrations, orchestrations.
Our architecture has to become mature, for business focus to be possible.
If we want to introduce DDD then our role is, not to force people around to use it, but to create environment in which it can emerge naturally.
We need foundation that solves the very basics of our problems like: resiliency, scalability and maintainability, and solving those is a promise of Messaging architecture. When this is achieved, Domain Driven Design comes forward as natural practice.
Messaging —The Return of OOP
I will start with quote that I always mention during workshops. The quote comes from Alan Key, Godfather of computer science, who coined the term Object Oriented Programming:
“Object oriented programming gets people to focus on the lesser idea. The big idea is Messaging.”
~ Alan Kay
Accordingly to Alan Key Messaging was the root of what OOP was meant to be, yet at some point we’ve deviated from that. We deviated into focus on Objects and references, instead of Communication and Messages. As a result we are now trying to achieve attributes of Messaging like resiliency, scalability and loosely coupling using development style that has moved away from it.
Messaging is return to what OOP was meant to be, which along the way we forgot. It creates an environment where better patterns and methodologies like DDD can come forward naturally.
Good reference on how to build Messaging system can be found in Enterprise Integration Patterns (EIP). Ecotone Framework implements EIP patterns in PHP and makes Messaging the main citizen in our Applications. It introduces it at the foundation level, which becomes a platform for communication between any PHP Objects.
Messaging with Ecotone
Ecotone brings Messaging based on Enterprise Integration Patterns (EIP) to PHP. EIP patterns introduce building blocks which allows to connect components seamlessly.
There are four main components on top everything is built:
Message — It’s a data record, which contains of Payload and Headers (Metadata) and is uniquely identifiable. Payload is data needed in order to handle given Message (like list of products we would like to order) and Headers are additional framework or application level metadata (like Message Id, Executor of the Message, Timestamp).
Message Handler/Endpoint — Is a place where we handle given Message and perform business related logic. This is a place where for example logic for placing an order will take place.
Message Channel — Message Handlers are connected to Message Channels in order to receive Messages. Channels may be synchronous or asynchronous and this indicates how Message will be handled.
Message Bus — Is used as a “Gateway” to begin Messaging flow. It converts application level data into Message and sends it corresponding Message Channel. This is how flow begins.
Placing an Order Scenario
Let’s explore scenario of User placing an Order for given set of products.
As a result of order being placed, we will send an Email and call external Shipping Service via HTTP to deliver the products.
This means there will be three actions:
- Storing an Order in database
- Sending an Email with confirmation
- Calling external Service via HTTP to deliver products
Let’s solve this without using any Messaging first. This way we will be able to see step by step, how we can refactor Non-Messaging to Messaging based code and what benefits it gives.
In the Controller we take the current user id and retrieve list of product ids from the Request, then we pass it to OrderService that does all the logic.
Let’s have a look on OrderService now:
This looks pretty straightforward. We store the order, send the email and call external Shipping Service. However this code does not support us in case of failures. Let’s dive into what may happen here, starting from sending an email:
Sending an email may fail due to network issues. If this happen, then the flow will stop and we will never deliver the order.
We should consider anything that goes over network as unreliable, as at some point of time in future it will fail. By being aware of that, we can start designing more robust applications which can recover from those situations.
So to solve that we could add handy Laravel function to apply retries:
This does not really solve the problem, it just makes it less likely to happen. If the Mail Server / Sendgrid / Mailgun will be down for longer period of time, we will not recover from this.
If email is not crucial we could use try-catch strategy to silent the error:
This of course is not bullet proof solution, it’s rather a solution of last resort, however for our scenario let’s consider it fine, as we have bigger fish to fry, delivering an order.
If delivering an order failed, we throw an exception, yet this means the order will never be delivered, which is crucial for the business.
To make it more resilient, we could start refactor the same way we did with email retries:
Again this does not really solve the problem, as in case of longer downtime of Shipping Service we will end up with Exception anyways. This time we can’t really ignore the failure, as Customer will not receive his goods.
This is a place where we most likely start to add some boolean flags in database to indicate if delivery was successful:
Now we need to write and maintain extra process, that look in database, if delivering order have failed, so we can trigger retries.
With this we are having more and more code related to recoverability now, the business logic starts to disappear in all the infrastructure related code.
Considering this is only single feature, it’s easy to imagine how much of this kind of code we will need for the whole system. We can’t focus fully on the business problems, when we need to solve resiliency this way.
From current perspective we won’t find good solution, as we either end up complicating the code more and more, or ignoring the fact that our system may fail. This is because we are solving side effects, not the root of the problem. And the architecture that solves this problem is Messaging Architecture.
By introducing Messaging we solve resiliency problem at the root level. This means by following Messaging principles, our architecture gets self-healing capabilities, therefore we don’t need to write recoverability code on our own. As a result we have much less code to maintain, our code becomes more readable and we have more time which we can invest into solving business problems.
Messaging way — Event Flow
We know that previous approach will make us pay huge price, therefore we want to solve resiliency problem differently. Let’s take a look on how we can modify our code to achieve resiliency using Messaging.
Let’s start by installing Ecotone for Laravel:
composer require ecotone/laravel
Firstly, we will introduce Event Message. Event is high level concept for Message which carry information that something in our system has happened. This means Events are used to summarize result of given action. In our example it will be information that the Order was placed:
When we’ve an Event Message, we can now subscribe to it using Event Handlers. Typically we will be using Event Handlers to trigger side effects of given Event. In our scenario we have two sides effect of order being placed:
- Sending an confirmation Email
- Delivering order to the Customer
To subscribe to given Event we will be marking given method with Event Handler attribute. Let’s now register two Event Handlers in our OrderService
As you can see we basically moved the logic to separate methods and marked method with Attributes. That’s enough to activate given method as Event Handler. The first parameter in Event Handlers tells Ecotone what Event Class given method is subscribing too, in our case it’s OrderWasPlaced. If you want you can move this method together with Attribute to different class and it will still work.
Ecotone follows declarative configuration with PHP Attributes. This means we won’t be extending or implementing framework related classes in our application level code. Instead we will using metadata in form of Attributes to state what we want to achieve.
To trigger Event Handlers, we need to publish OrderWasPlaced event and we do it using Event Bus. Event Bus like any other Buses (Command/Query Bus) are available automatically after installing Ecotone package.
As you can see our code have not changed much. We just moved the logic to two separate methods, marked it with EventHandler and triggered Event Bus. We can run this code now.
The current code will work, but it does not make system resilient yet. It will execute Event Handlers synchronously. This means it will still have the same problems as the previous example.
To achieve resiliency we will make Event Handler Asynchronous. This way we will be able to use powerful Messaging concepts like Failure Isolation, Automatic Retries and Dead Letter.
By adding Asynchronous attribute we are stating that this Event Handler should work asynchronously. We gave information to Ecotone that whenever Event is meant to be delivered to this Event Handler, it should first go through given Queue with name “asynchronous_queue”.
To register Message Queue and we can use Laravel Queues.
ServiceContext is special Attribute in Ecotone, that stays this method returns configuration. In above method we are returning configuration for the Queue using Laravel Queues (More integrations are available). We are stating that we want to use “database” connection and Queue with name “asynchronous_queue”.
To execute Message Consumer that will fetch Asynchronous Messages and execute Handlers, we will be using bellow command:
php artisan ecotone:run asynchronous_queue -vvv
If you want to explore more about asynchronous handling you can go to documentation page.
In Ecotone, queue implementation is decoupled from Event Handlers. Therefore we can change the implementation of the Queue without changing application level code.
It’s important to understand what happens under the hood when we send Event Message. In most of the implementations of Event Bus, there is lack of failure isolation. When we send an Event Message, single Message goes to the Queue and triggers all related Event Handlers.
This put on Developers the need to analyse each of the scenario if retry is safe to be done. As if we retry failed Message, we will end up re-triggering all Event Handlers. This produce unexpected side effects, where Event Handler that was previously successful will be triggered more than once. This is why Events are often considered unsafe to retry, which is true from perspective of this implementation.
This is not the case in Ecotone however, as Ecotone deliver separate copy of Message to each of Event Handlers.
This way we ensure the isolation of processing, if given Event Handler will fail, we will retry only the Handler that failed in full isolation.
Before we will discuss how those retries works, let’s have a final look on the Order Service:
As you can see each method has concrete responsibility. Placing an order stores the order and inform that the order was placed. The side effects like confirmation email and delivering the order are hooking into the flow by subscribing to the Event. The same way we can add more side effects without altering placing the order.
We wrote code that follows Single Responsibility Principle, yet that was no the aim. The aim was to provide isolation for failures thanks to Messaging, and SRP is just side effect. After awhile of working with Messaging you will find out that a lot of good practices becomes part of your daily development practice without thinking about them, because Messaging is what OOP meant to be.
Failure and retries
At the beginning of the article we were doing retries by handling them manually:
This was pushing us into handling failures in the application level code. With Messaging we’ve opened possibility to do this kind of things automatically without writing any additional code. For this we’ve two options:
Instant retries works the same as our above code. So they wrap given Event Handler and in case given Exception happen, retries will instantly kick in:
The only thing we need to do to configure them is to provide Ecotone with configuration for which Exceptions it should happen and how often:
Delayed retries are more powerful concept, as they give us possibility to retry given Message with delay. It helps to solve scenarios where external Services are down for longer period of time, as in that case Instant Retries won’t help us.
We may set up, how many times we should retry, what should be the time between retries and if each try should multiply the time:
The “errorChannel” is the place where Error Message will be pushed. We define the name in here because we may want to set up different configuration per Message Consumer.
To use above configuration as default one, configure “defaultErrorChannel” to “errorChannel” in your Laravel’s configuration for Ecotone.
There will be cases that no retries will help us auto-recover, this may be because External Service is down for really long time, or we’ve introduced a bug in application or integration with external Service is incorrect. In those situations retries will not solve the problem, so we need different solution.
Ecotone provides Dead Letter to store Error Messages in database. To make use of it we need to install dbal integration:
composer require ecotone/dbal
After installing we get ability to store Error Messages in the database:
We store this Message for a reason, as we don’t want to write code to recover email or retrigger integration. Having this Message in the database, allows us to three step to recover:
1. Investigate why it failed (Error details are stored with Message).
2. Do the fix and release it on production.
3. Replay the Message back to the Queue.
To view, delete/replay the Message from Dead Letter we may use inbuilt Command Line Interface:
To configure dead letter, we need to modify last parameter of our retry configuration:
Actually we may connect multiples Ecotone based Applications and review Error Messages from one place using Ecotone Pulse.
Safe Message Sending
We’ve isolated failures and enabled retries, whenever failure happens it will be automatically recovered and if unrecoverable error happens we will store it in Dead Letter. Yet there is one more part which we can make more resilient — Sending Messages.
We are having three actions here, we store Order in database and we send two Messages to the Message Queue. There may be a situation where, sending to the Queue will fail. In that case we could end up in inconsistent state, where order is stored, confirmation is sent, yet order is never delivered.
In our case we are sending Messages to the Database Queue using Laravel Queues, so we can wrap everything in transaction and commit it together.
With this we will ensure we always end up in consistent state, as everything is committed together. Yet we’ve introduced DB:transaction which is infrastructure level code in our placing order process, and this will happen for each situation when we will want to use transactions.
We can refactor this code and get rid of transactions from the application level code using Command Messages.
To avoid working with database transactions directly, we will introduce Commands. Command are automatically wrapped in transactions, when ecotone/dbal module is enabled.
Command Messages describe a specific action to be taken (like
RegisterProduct). They carry an intention of what we would like to happen.
If you’re unsure, if Message is an Event or Command, you may ask a question: if this Message stating that something happened (Event) or asking for something to happen (Command)?
To create an Order via Command Message, let’s first create an Command Message, this will wrap parameters we are passing to OrderService::place: “userId” and “productIds”.
Having Command defined, we can now define Command Handler for it.
We have only packed the parameters from the method into an PlaceOrder class and marked the method with CommandHandler attribute. That’s enough to activate this method as Command Handler.
Let’s now now send Command from the HTTP Controller. To send an Command Message we will be using concrete type of Message Bus — Command Bus.
Right now transactions will be automatically started for us whenever we place an Order. Any future Command will be wrapped with transactions too, this gives us default resiliency, so we don’t need to think about it anymore.
We’ve discussed Instant Retries in case of Asynchronous Messages, but we may enable it for synchronous actions too, like placing an Order:
This will secure us from from the situation when database connection is lost, dead lock occurred or any other exception happen which we would like to retry. So by adding above configuration, we secure that in case of database connection error instead of losing an Order, we will do instant retry to recover.
Instant Retries and automatic Database Transactions are handled with Interceptors. Interceptors allows us to hook into the flow and do additional logic. This way we can handle cross-cutting concerns with ease. If you want to explore more visit documentation page.
Your Jobs are your Commands
We use Laravel Jobs to process asynchronous time consuming tasks, yet in Messaging world Jobs are still Messages with specific intention.
The intention may be to upload a file, resize image, send an email. In case of Messaging we stop thinking about Asynchronous Messages as Jobs, as they are simply Command Messages with another use case for them.
From now on, we don’t need to write recoverability code as we have solved resiliency on foundation level. Resiliency became part of our architecture, which is now the default way of how we develop things. All future features will be able to self-heal by default now.
The time we’ve regained for everyone in the team is huge:
- Developers can write, maintain and test business related code, as recoverability is already solved
- Developers can spend less time debugging and looking for issues, as each feature can now easily be made resilient
- Team on the meeting and in PRs can discuss, review and focus on higher level business code instead of recoverability code
We made a ground work to focus on the business part of the system now, so let’s discuss Domain Driven Design.
Domain Driven Design
We already did few steps into business oriented software. If we take a look on our code, we will see it does focus on what Business would like us to do:
This code is easy to follow and understand and even so we are using Message Queues for asynchronous processing, we don’t mix it with application level code.
What’s also important, we’ve started to use business terms in the code base by introducing Command - “PlaceOrder” and Event — ”OrderWasPlaced”.
Both Commands and Events are using business language and will be understandable by Business when we will speaking about those.
The next step is encapsulate logic with Aggregate and get rid of orchestration level code.
Messaging with Aggregates
If we allow to create our Models directly we open possibility for creating incorrect Models. Nothing really block us to pass products, yet not calculate the total price:
This code will store Order in the database, yet the total price will be incorrect and OrderWasPlaced Event will never be published. As you can see, we’ve not protected our system from incorrect usage.
Using PlaceOrder Command we can be sure, that wherever in code we will be sending this Command, Order will be stored in database in correct state and Event will be published. That’s why it’s important to not bypass Messaging and use it as our API, to protect correctness of our data and that expected behaviour is executed.
The important building block in DDD is Aggregate. Aggregate are Models rich in behaviour, so in our case that will be Eloquent Models that expose business related actions. Aggregates hold business logic and protect correctness of the data. This means we can move the logic we’ve placed in “OrderService::place” directly into Order Aggregate.
As you can see we’ve exposed creation of Order Model via Command Handler directly in Aggregate. For that we are using static method which returns new instance of Order. It becomes explicit in the code that this Command Handler is responsible for creating new Orders.
Let’s consider possibility of new feature to cancel the Order. Standard implementation could looks like this:
The above code is written to do three steps:
1. Fetch Aggregate
2. Change state of the Order
3. Save Aggregate
The first and third point are actually an orchestration level code, and we don’t need to write it. And this can be done automatically by Ecotone:
We exposed cancel method directly on Aggregate. If there would be any business rules to verify, we could do it on the Aggregate level to protect this action from incorrect usage.
To execute this action we will be using Command Bus with method sendWithRouting.
We are passing special metadata parameter, which provides “aggregate.id”, this way Ecotone knows which Aggregate instance it should fetch from database and execute.
Domain Driven Design Summary
By introducing higher level concepts like Commands and Events we start to use Domain Language and create Business flows. Aggregates combined with Command Handlers help us to protect our business rules and make business logic explicit.
We can publish Events directly from Aggregates and hook side effects using Event Handlers. This way we can build business flows with ease.
All Building Blocks (Commands, Events, Aggregates) are glued together seamlessly using Ecotone’s Messaging. This helps us fully focus on the business part of the System.
This article had a lot of information, as the concepts explained in here are depth and can’t described in few sentences. During workshops it take me around 3–4 hours to explain theory and do exercices on this subject, therefore give yourself time to understand materials in this article. Some of them may require re-reading to “sink in” or doing actual coding in order to feel it in the first hand.
You could use Ecotone without the resiliency part, by bringing the features that you’re mostly interested in. For example some developers use Ecotone’s Event Sourcing Module without the Resilient Messaging part. Remember however, that it’s the environment that creates the results. If we want our systems to focus on the Business, not technical parts, we need to create environment where it can happen. And to do it, we need resiliency as part of our architecture, not as part of the business feature we need to build.
When integrating with Messaging for the first time, do not rewrite your whole system at once, give yourself a time to experience it on small part of the code base. What we did in this article is a re-write of one functionality, and this is how you can start in your own system. Having single feature on production will give everyone in the team time to experience it. And when you feel confident about the solution, you can decide on rewriting next features. This is the safest way to do the transition.
The more you will be using Messaging with DDD, the more you will see how well those pieces works together to help us build reliable, scalable and business oriented applications.
Thank you for reading :)