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