Building Blocks: Exploring Aggregates, Sagas, Event Sourcing with Ecotone
Learn how building blocks enable developers to build domain-focused applications while abstracting away the complexities of integration.
In the world of software development, we often find ourselves juggling various technical concerns, infrastructure considerations, and complex integrations. However, what if there was a way to focus primarily on the business logic and let a framework handle the heavy lifting of interconnecting the different components?
In this article, we’ll dive into the concept of building blocks in Ecotone and how they enable developers to build resilient, domain-focused applications while abstracting away the complexities of integration and infrastructure.
Building Blocks: Specialized Cells
At the heart of Ecotone lies the concept of building blocks, analogous to the specialized cells in our bodies. Each building block represents a specific functionality, such as Command or Event Handlers, Aggregates, Sagas and more. These blocks provide solid groundwork built on resilient messaging, allowing developers to focus on the business logic and flow of their applications.
Just as cells in our bodies perform specific functions, Ecotone’s building blocks do the same in their respective areas. For example, Command Handlers handle incoming commands, Event Handlers react to events, and Sagas orchestrate complex workflows. These specialized cells collaborate and communicate seamlessly, forming the organs of your application. Just as organs work together harmoniously, Ecotone’s building blocks join forces to create robust and interconnected, yet decoupled, applications.
In our cellular analogy, cells send messages to other cells, yet they are not burdened with the responsibility of message transport. Similarly, in Ecotone, you, as a developer, make use of building blocks to provide business logic and let the framework handle the intricate details of message routing and delivery. Your building block and messages are POPOs(*Plain Old PHP Objects*)
, they do not extend or implement framework specific classes.
Those powerful conceptsdischarge
you from the complexities of infrastructure, and enable you to work at the business level with a sense of ease and abstraction.
E-Commerce Platform
Let’s dive into an e-commerce platform and start using Building Blocks to build the application.
We’ll start with the registration of a new Customer.
Customer Aggregate
In our application we will start by setting up the Customer Aggregate.
Aggregates are behaviour rich Entities, that encapsulate data and expose a public API to interact with it.
This could be achieved with a Service/Application layer, where we could expose a series of actions:
The above solution require us to write orchestration code.
A CustomerService class uses a repository to fetch and store Customer, and an additional delegation layer that is not business related code.
And even if this is possible in Ecotone, there is a better way to deal with this, that allows us to drop all of the boilerplate code.
With Ecotone, the Aggregates can directly be used as the Command Handler. Ecotone will use the Repository in order fetch and save the Aggregate just like in CustomerService above. As a result we get rid of the boilerplate in the Service/Application layer. Less code to write, less code to maintain.
Ecotone provides Repository integration with Doctrine ORM and Eloquent Models to enable them as Aggregates. Yet if we want to write our own Repositoriy implementation, this is albo possible, obviously.
Important in Message-Driven architecture is testing. Without proper testing support, connecting components and ensuring that everything works as expected become painful.
Therfore Ecotone provides full testing support where we can isolate the components we need to test.
With Ecotone Lite we can bootstrap an Ecotone application with a given set of classes. It supports testing, enables us to send Commands and assert the state afterwards.
As the Command Handlers have been implemented within the Aggregate, the requirement is functional. No more need to create multiple layers and transformations. We focus directly on the domain and we can easily test it.
Regardless from where the code is called, even from a Controller, its usage will remain the same and with the simple test above, we have proven it will work as expected.
Product — Event Sourced Aggregate
In order to create new products for our e-commerce shop, we need to fulfill a couple of requirements.
- Product has a price that can change;
- By default, thhhe Product is hidden and not available in the shop;
- Product must be approved first, before it is possible to sell it;
- We should keep history of all the product’s changes;
- We should be able to provide a list of all the products that were not verified yet, so the business can check them out;
- In future we may provide different filtering and text searches;
As we want to keep the history of all changes, event sourcing will do the job, as it keeps track of all the changes.
In case of Event Sourcing instead of changing internal state, we are returning list of events. Those events are stored in Event Store.
Ecotone provides Event Store out of the box for PostgreSQL, MySQL or MariaDB.
Yet if we want we can write our own Event Sourcing Repository.
Normally, we would have to rebuild the state inside the Aggregate from past events using “EventSourcingHandler” methods.
But in our scenario, we don’t even need to do that, as there are no business invariants (“ifs” in Command Handler that protect the aggregate) that need specific state to be rebuilt.
We can test it as we did above with Ecotone Lite:
Product must be approved first, before it will be available in the shop.
To handle this requirement, let’s start with command ApproveProduct command:
By parsing “productId”, Ecotone is able to figure out which Aggregate should be fetched for the execution.
And then the Command Handler in the Product Aggregate:
We may run the test with the In Memory Event Store, this will also ensure that events can be serialized correctly.
We’re providing initial state using “withEventsFor” method, so we can trigger the command and verify the resulting event afterwards.
As we run this test using In Memory Event Store, we also need to provide Converters for objects, if we need to provide custom serialization and deserialization mechanisms e.g. “UuidConverter”.
All events will be serialized to In Memory Event Store. This way, the execution mimics as much as possible what would happen in production.
If we will take a look at the “ApproveProduct” command, we will see that it only contains the “productId”. Not exactly what one would a call a rich domain model.
This also means that we have created “ApproveProduct” command just to match the Framework’s needs, which is not ideal.
Once more, Ecotone takes care of this and avoids this pitfall. Let’s redesign our Approve Command Handler:
We’ve defined a routing key for the Command Handler named “product.approve”. We can trigger this Command using this key now.
By passing metadata “aggregate.id” to the CommandBus we are telling Ecotone, which Product instance it should fetch and we can get rid of Commands that are carrying identifiers only.
Routing for Message Handlers can be used to trigger Command Handler without Command class, yet it can also be used to execute command in given format e.g. json/xml directly from Controller. We can pass incoming Request’s data, tell Ecotone what Media Type it contains and Ecotone will deliver the Message, convert it accordingly and execute your Message Handler.
Unapproved Product List — Projection
We should provide list of not approved products so the business can see which products it should still verify.
In future we may provide different filtering and text searches.
Now we are able to create and approve products, we can list the products that have not been approved yet:
We provide a Projection that is derived from Product’s Event Stream.
Each method marked with “EventHandler” defines what event it would like to subscribe to.
We are using DocumentStore to store the projection’s Read Model, as it provides In Memory implementation for tests. This is Ecotone’s abstraction to store array/objects in key-value storage. We could use however whatever persistance mechanism we’d like.
The Document Store provides implementation for In Memory used directly in tests and for real databases likes PostgreSQL, MySQL or MariaDB.
As you can see, the Document Store is the Event Handlers’s constructor’s second argument
By marking parameter with “#[Reference]” we are telling Ecotone that this is a Service and should be injected from Dependency Container.
Then we can test the projection using Ecotone Lite:
With Event Sourcing we can build as many Read Models (Views), as we need. In case of Ecotone we can combine Event Streams from different aggregates, if we need more aggregated view.
With this we build for the future: When new requirements emerge, we will be able to build new views without changing our domain model.
Scaling with Cells and Building Blocks:
As cells combine to form organs, the same principle applies to building blocks in Ecotone. These blocks join together to build the various components and functionalities of your application. And just as our bodies can scale and grow, Ecotone enables the scaling of your applications by facilitating communication between different building blocks and even across multiple applications. The framework handles the intricacies of message transport, so you can focus solely on the logic and flow of your business processes.
It’s crucial in messaging based systems to have full testing support, otherwise system based on messaging easily get messy and will give the impression of being hard to understand and maintain.
That’s why Ecotone provide full testing support.
We already discussed a little of Ecotone’s testing support, however there is much more to it. With Ecotone we get full testing support. We can isolate groups of classes to use in the test, run Message Consumers (workers) synchronously or asynchronously, trigger Consumers with in Memory channels or real Message Broker channels etc.
Due to Ecotone’s messaging nature based on Enterprise Integration Patterns, it becomes really easy to test any scenario you could think of.
Let’s reconsider our example with the Unapproved Product List Projection.
In the above test scenario we’ve included the Projection and the Aggregate in the test. Then, the triggered Command will results in an Event, and finally the projection can be materialized.
By default, in test mode, all projections are running synchronous, yet we can switch to asynchronous, if we want to.
Combining classes under test is a powerful concept: It allows us to only include the classes that are relevant to a given scenario and test full flows in isolation.
With Ecotone testing support, it becomes really easy to write unit, integration and acceptance tests. This will ensure that all your business flows are working as expected.
Placing an Order
We want to include one more functionality to our application - placing an order.
There is one caveat in the order process: Customer could see that products are available in stock, however when the order is placed, the stock could already have changed. If products are in stock when the order is placed, then everything is fine. If, however, products are out of stock, we want to retry reserving them one hour later. If the second attempt still fails, we should cancel the order.
We will be calling external service over HTTP to reserve the products in stock. This our interface for doing so:
We will just mention the Order Aggregate, implementation will be as straight forward as the Customer Aggregate.
We can publish events from State-Stored Aggregates. The difference between this and Event Sourcing Aggregate is that for latter, events are kept in the Event Stream.
“OrderWasPlaced” will now begin our OrderSaga.
“whenFirstAttempt” method will be called after Saga is started and is triggered asynchronously. This way we ensure, that even if it were to fail, it would not affect the Saga's storage. On failure, we can use messaging to retry
“whenSecondAttempt” method will be called after Saga is started and is triggered asynchronously. This way we ensure, that even if it were to fail, it would not affect the Saga's storage. On failure, we can use messaging to retry.
**“whenSecondAttempt”** method will be called one hour after the Saga was started. We can then verify our business requirements and check if we can reserve the products this time.
This way, we have actually easily connected two different flows.
So whenever Order is placed, this Saga will be started.
As a result of starting Saga, we will run one Event Handler right away, asynchronously, and the second one, an hour later.
Delaying Message Handlers is a powerful concept. Thanks to the “Delayed” attribute we can make business delays explicit in the code.
Making Event/Command Handlers Asynchronous is matter of adding an `Asynchronous` attribute. Each Asynchronous Message Handler receives a copy of the Message. And each Handler works in full isolation (which enables safe retries) and allows us to define custom ways of handling.
For example, one Event Handler can be synchronous, the other asynchronous or we may delay some Handlers or add different levels of priority.
Of course Ecotone provides an easy way to test asynchronous Handlers, so we can verify, if our Saga runs as expected.
Using “releaseAwaitingMessagesAndRunConsumer” we make it easy to test out really complex scenarios, where time is involved. This way we can trigger events awaiting given delay and verify the state.
There are no constraints in writing tests in Ecotone. It is possible to test and connect basically every possible scenarios. No matter if the code is asynchronous, make use of delays or multiple Message Handlers. The level of difficulty does not matter, because Messaging is Ecotone’s second nature.
And having such support is crucial for building maintainable software that we can put trust.
Production run
We’ve been building models and testing them. Development is straightforward, because all we do is building business related code.
There is no “glue” code, no framework related code etc.
What we’ve been creating so far, is already production ready and can be deployed to production.
We did not need to define any custom repositories because Ecotone provides built in ones for: Event Streams, Sagas or State-Stored Aggregates. Nevertheless, if we need to, we can switch to our own custom ones.
The code from this article can be found in Github Repository.
You will find all the tests under “tests” catalog and you may run the code in production environment using “run_example.php”.
Building Blocks Conclusion:
Ecotone’s building blocks provide a powerful and intuitive way to develop resilient, decoupled and domain-focused applications. Just like interconnected cells in nature, these building blocks seamlessly communicate with each other, allowing developers to focus on the business logic while leaving the infrastructure complexities to the framework.
By abstracting away the transport of messages and providing purpose-built cells, Ecotone enables developers to unlock a joyful and efficient development experience.
So, embrace the interconnectivity of Ecotone’s building blocks, and embark on a journey of resilient and enjoyable application development.