16
What is event-driven? Order#value example
Some time ago, I have mentioned to you that there is this sample DDD/CQRS application.
We've (Arkency people + Arkademy subscribers) been working on it and it evolved into something new.
It's no longer just a sample application. It's now part of a bigger project called:
Arkency Ecommerce
Ecommerce codebase which makes developers happy.
The goal is exactly this, create such a codebase that will make developers working on it (extending, integration, customising) truly happy.
I honestly don't know if this will be a framework, or a set of libraries or a customisable scaffold or a code generator.
Yet.
What I do know is that the current popular ecommerce platforms are targeting business people (nothing wrong with that), but not always care about programmers (sad).
You either need to use some overcomplicated codebase and patch it so that it serve your custom needs or you need to make a thousand of API calls because integrating 23 different SaaS providers is apparently the way to go in 2021.
Welcome to the integration hell.
What's the alternative?
A simple, event-driven codebase, consisting of clear and mostly generic bounded contexts like: pricing, payments, ordering, inventory, catalog, crm.
A number of preset read models available either via API or via Hotwire/Stimulus approach.
A codebase which you can fork and then customize by implementing the process managers on top of it.
That's the goal of Arkency Ecommerce.
Modulith, not microservices.
Events, no coupling.
Event sourcing, no ORM.
Mutation testing coverage, no dead code or untested areas.
We want to have an easy ecommerce product line.
Are we there? Nope.
Is it useful already? Yes.
To my surprise, there are already 2 companies which are forking this repo to start their efforts on top of that. They do want to contribute back, which is awesome.
Let me explain the modularity goal.
Almost every programmer would say that modularisation is a good thing.
Having modules means having smaller scope. It means easier testing. It also means less bugs in the end.
It's not so easy to find the right boundary for the modules. Some modules are too small, some are too big. Some modules couple certain things together.
Let's say the modules are more or less "right".
What is the challenge now?
Composability
How to connect modules so that together they create working system?
Here's what I did with Arkency Ecommerce.
I split the business logic into business modules following the patter of Domain-Driven Design - Bounded Contexts.
Then I went for the Read/Write split, following the CQRS approach.
This resulted in a number of smaller modules:
- Ordering
- Payments
- Pricing
- ProductCatalog
- CRM
plus some UI modules:
- Orders
- Products
- Customers
How do they talk to each other? How do they compose together?
via EVENTS
and COMMANDS
Each module can be told what to do using commands.
Each module publishes events as a result of executing commands.
Events and commands are the way to compose modules together.
Let's look at a simple concept of Order's total value.
There are several modules involved:
- Pricing needs to calculate the value
- Payments needs to know how much to charge
- Orders (the UI modules) needs to display it
cqrs.subscribe(
-> (event) { cqrs.run(Pricing::CalculateTotalValue.new(order_id: event.data.fetch(:order_id)))},
[Ordering::OrderSubmitted])
This line of code connects Ordering with Pricing without them knowing about each other.
cqrs.subscribe(
-> (event) { cqrs.run(
Payments::SetPaymentAmount.new(order_id: event.data.fetch(:order_id), amount: event.data.fetch(:amount)))},
[Pricing::OrderTotalValueCalculated])
This line connects Pricing with Payments.
Again, without them knowing about each other.
This, my friend, is the beauty of event-driven architectures.
This is the reason I felt in love in event-driven DDD.
Such modularity, such isolation, such independence of modules - this all create robust software.
That's the foundation for Arkency Ecommerce.
You can see the event-driven flow in more details in this 5 minutes video on Arkency Youtube:
Here is the whole modules/events setup:
class Configuration
def call(event_store, command_bus)
cqrs = Cqrs.new(event_store, command_bus)
Orders::Configuration.new(cqrs).call
Ordering::Configuration.new(cqrs).call
Pricing::Configuration.new(cqrs).call
Payments::Configuration.new(cqrs).call
ProductCatalog::Configuration.new(cqrs).call
Crm::Configuration.new(cqrs).call
cqrs.subscribe(PaymentProcess.new, [
Ordering::OrderSubmitted,
Ordering::OrderExpired,
Ordering::OrderPaid,
Payments::PaymentAuthorized,
Payments::PaymentReleased,
])
cqrs.subscribe(OrderConfirmation.new, [
Payments::PaymentAuthorized,
Payments::PaymentCaptured
])
cqrs.subscribe(ProductCatalog::AssignPriceToProduct.new, [Pricing::PriceSet])
cqrs.subscribe(
-> (event) { cqrs.run(Pricing::CalculateTotalValue.new(order_id: event.data.fetch(:order_id)))},
[Ordering::OrderSubmitted])
cqrs.subscribe(
-> (event) { cqrs.run(
Payments::SetPaymentAmount.new(order_id: event.data.fetch(:order_id), amount: event.data.fetch(:amount)))},
[Pricing::OrderTotalValueCalculated])
end
end
Let me know if any of the concepts here sounded interesting to you - I'd be happy to explain more.
16