Process Managers

Background

Domain

Domain Driven Design(DDD) is an approach to building software that tries to tackle the complexity of the business by focusing on the core domain, building an evolvable model of the domain, and involving domain experts in the evolution of the model to develop and ensure a consistent understanding and language.
You can think of a domain as a problem space. For example, allowing people to order food online from their local takeaway, and giving restaurants an online presence and the ability to process orders with no upfront investment in infrastructure is the problem space of JUST EAT.

Subdomains

A domain is broken down into smaller subdomains to simplify the understanding of the problem space. A few examples of subdomains in JUST EAT would be restaurants, ratings, order processing, payments, menus, business intelligence, finance, marketing, etc. Some of the domains are more important than others and some domains are there just to support others. Accounting and finance are supporting subdomains for example, but the order processing and customer communication are core subdomains.
The subdomain that differentiates a business from competitors – or that makes most of the money for the company – is called the core domain. Usually the core domain gets the most investments in terms of development time, care and attention.

Bounded context

In an ideal world, once the subdomains and the core domain are identified, teams of developers and domain experts are formed around them to develop the model, the implementation and the language specific to each subdomain.
The team, the model and the language they create to solve the domain problem belongs to the solution space, and they form what is known in DDD as a bounded context.
It’s ideal for a bounded context to map exactly to a subdomain to avoid confusing language in the team. A team working on both the finance and restaurants subdomains would always have to qualify the term ‘account’ in their discussions, for example, because both subdomains are using the same term but they view it differently.
In reality though, this is difficult to achieve and most of the time bounded contexts cut across multiple subdomains.

Implementing Domain Driven Design

Core domain

It’s very rare that the core differentiator of a business comes from a single bounded context inside the business. More often than not the core domain comes from an integration of multiple parts of the business.
Imagine you place an order for a product and the supplier doesn’t have it in stock. The supplier could just delay the order until the stock is replenished or simply reject the order. Neither of these outcomes are ideal for the customer.
There is an opportunity for improvement though. The supplier could just upgrade the customer to a better spec of the product that is already in stock and tell the customer that if they still want the original product to send the upgrade back.
This is way better than the previous scenario and it becomes the core domain as it keeps everyone happy.
To be able to do this more than one part of the business needs to be involved to discover what the possible upgrades are available, what their prices are and how quick can be delivered.

Dealing with time and thinking in loops

A long-running process introduces complexity with regards to timing. Say, the order is placed but the payments context never confirmed that the actual monetary transaction happened. Without the payment confirmation the process can’t go further. An indefinite wait could be fine in some cases but most of the times there needs to be a definite answer as to whether the product can be delivered or not. So we need a way of notifying the process in the future to cancel the order, if the payment has not been confirmed in, say, two minutes.
In order to deal with these timing issues, we need to introduce an alarm or timeout service that can delay-send messages. When an order is placed we tell the alarm service to send us a cancellation message in the future. So if no payment is made in the next couple of minutes we’ll receive the order cancellation message from the alarm. If however the payment was made and the order was advanced through the process, when the cancellation is received it is ignored right away.
Thinking in time loops when designing a process is very powerful because if something unexpected happens, the process can enter a state where manual intervention is required so people can pick up the work and fix the unexpected problem. The ultimate goal of process automation is not to totally replace manual interventions but to automate as much work as possible.

The order process manager

In the next section I’m going to introduce the process manager pattern and give some code examples in C#. While there are many ways of implementing the process manager pattern I’ll keep it simple and try to minimize the dependencies on other frameworks or platforms.

The process manager pattern

Enterprise Integration PatternsHow do we route a message through multiple processing steps when the required steps may not be known at design time and may not be sequential?
Enterprise Integration Patterns
The main purpose of a process manager is to encapsulate the process specific logic and maintain a central point of control. It’s initiated by a trigger message which could be an event coming out of a bounded context.
The process manager decides what to execute next once a process is completed becoming a hub for start and finish types of messages. It can also become a performance bottleneck when parallel processes try to communicate back their state.

Here’s the simplest implementation of process manager. The When methods will be called when something interesting happens.

public class OrderProcessManager
{
    public enum OrderProcessState
    {
        NotStarted,
        OrderPlaced,
        PaymentCompleted,
        OrderDispatched,
        OrderDelivered
    }
    public OrderProcessManager()
    {
        State = OrderProcessState.NotStarted;
    }
    public Guid Id { get; private set; }
    public OrderProcessState State { get; set; }
    public void When(OrderPlaced @event)
    {
    }
    public void When(PaymentCompleted @event)
    {
    }
    public void When(OrderDispatched @event)
    {
    }
    public void When(OrderDelivered @event)
    {
    }
}

The trigger message is the OrderPlaced event.

public class OrderPlaced
{
    public Guid OrderId { get; set; }
    public Guid RestaurantId { get; set; }
    public Guid UserId { get; set; }
    public List<OrderItem> Items { get; set; }
    public decimal Price { get; set; }
    public Address DeliveryAddress { get; set; }
}
public class Address
{
    public string Street { get; set; }
    public string PostalCode { get; set; }
}
public class OrderItem
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

All this is plain old c# code which is great but there is some infrastructure missing here. How does the OrderPlaced event get delivered and who creates a new instance of the OrderProcessManager class?
I’m not going to go into the infrastructure details here but you can use a Service Bus implementation to deliver the messages for you. In my example I’ve used JustSaying which is an open source library that runs on top of Amazon Web Services and delivers messages for me using Simple Queue Service and Simple Notification Service.

State management

One other responsibility of the process manager is to maintain state between the message loops. It does this by using some sort of persistent store like a SQL database or a NoSQL/Document database.
All messages come to a message handler which in this case plays the role of a Process Manager factory and message router. Here is a simple implementation of the OrderPlaced handler. I used the name router as that is its main purpose, to find the right instance of the Process Manager and forward the message to it.

public class OrderProcessRouter:
        IHandler<OrderPlaced>
{
    private readonly IRepository<OrderProcessManager> _repository;
    public OrderProcessRouter(IRepository<OrderProcessManager> repository)
    {
        _repository = repository;
    }
    public bool Handle(OrderPlaced message)
    {
        var pm = _repository.Load(message.OrderId);
        if (pm == null)
        {
            pm = new OrderProcessManager();
        }
        pm.When(message);
        _repository.Save(pm);
        return true;
    }
}

A few things are happening here. Firstly, correlation. Every process manager instance needs to be identified and re-loaded when a message is received. To be able to identify it, every message needs to share a correlation ID with the process manager instance. In this case I am using the OrderId as the correlation ID.
Secondly, idempotence. Even though the OrderPlaced event is the trigger for a new instance of process manager, the router is first trying to find the process manager by ID. If the process for that order has already been started it means that the message we received is a duplicate message. In distributed messaging systems like AWS SQS you will occasionally get a duplicate message and you have to deal with it. Most distributed messaging system guarantee at-least-once delivery for messages.
Thirdly, message forwarding/routing. The router simply hands over the message to the process manager here.
Lastly, persistence. The newly created instance or the process manager is persisted into the repository. You could have a race condition here, where another router on another machine is persisting the same instance so you need to implement some versioning in the process manager and enforce concurrency checks in the repository. This is critical.

Event processing

Let’s have a look at what happens when the process manager receives the OrderPlaced event.

public class OrderProcessManager
{
  public Guid Id { get; set; }
  public OrderProcessState State { get; set; }
  public int Version { get; set; }
  public List<Command> CommandsToSend { get; private set; }
  public Guid RestaurantId { get; set; }
  public Guid UserId { get; set; }
  public List<OrderItem> Items { get; private set; }
  public Address DeliveryAddress { get; set; }
  public decimal Amount { get; set; }
  public void When(OrderPlaced @event)
  {
      switch (State)
      {
          case OrderProcessState.NotStarted:
              State = OrderProcessState.OrderPlaced;
              Id = @event.OrderId;
              Items = @event.Items;
              RestaurantId = @event.RestaurantId;
              UserId = @event.UserId;
              DeliveryAddress = @event.DeliveryAddress;
              Amount = @event.Amount;
              SendCommand(new ProcessPayment
              {
                  OrderId = @event.OrderId,
                  RestaurantId = @event.RestaurantId,
                  Amount = @event.Amount
              });
              break;
          // idempotence - same message sent twice
          case OrderProcessState.OrderPlaced:
              break;
          default:
              throw new InvalidOperationException("Invalid state for this message");
      }
  }
}

 
The process manager stores all the information and changes its internal state to OrderPlaced. It also ‘sends’ a command to payments to process the order passing a few important details about the order. The SendCommand function below is just adding the command to a list of commands to be sent so the name is misleading but I could not come up with a better one. When the process manager instance is saved the repository will use a bit of infrastructure to send the commands. I am not going to show that here though.

private void SendCommand(Command command)
{
    CommandsToSend.Add(command);
}

 
At this point the ‘payment loop’ is started. In the payments bounded context many things will happen. For simplicity sake I didn’t involve an alarm service here but ideally you want the process manager instance to be notified if nothing happened with the payment for two minutes. One way to do it would be to add another command to be sent to the Alarm that contains the necessary details.
Once the payment is completed successfully a PaymentCompleted event comes out of the Payments bounded context. The Process Manager now moves to the next step which is to dispatch the order to the restaurant for delivery.

public void When(PaymentCompleted @event)
{
    switch (State)
    {
        case OrderProcessState.OrderPlaced:
            State = OrderProcessState.PaymentCompleted;
            SendCommand(new DispatchOrder
            {
                OrderId = Id,
                RestaurantId = RestaurantId,
                Items = Items.ToList(),
                Amount = Amount,
                DeliveryAddress = DeliveryAddress
            });
            break;
        // idempotence - same message sent twice
        case OrderProcessState.PaymentCompleted:
            break;
        default:
            throw new InvalidOperationException("Invalid state for this message");
    }
}

 
Same pattern here. The state is changed and a new processing loop is started, the order dispatching loop.
You can find more details about the implementation in this article here https://github.com/justeat/ProcessManager