Refactoring to Microservices - Introducing a Process Manager
A while ago I described the first part of our journey to refactor a monolith to microservices (see here). While this was a useful first step, a lot can be improved. I was inspired by Greg Young's course at Skills Matter, see CQRS/DDD course. Because I think it’s useful to reflect on the steps you take when changing software architecture, I’ve set a couple of milestones and will report on each when I get there. The first goal is to introduce process in our domain and see what happens.
If you’re interested in details, you can find the code I’m referring to here:
code on Github.
Clone the project and then check out the shopManager tag
git clone email@example.com:xebia/microservices-breaking-up-a-monolith.git git checkout -t origin/shopManager
The code can be found in the services/messages folder.
The problem with our first implementation is that it misses a concept: there is no notion of a process. In earlier solutions the process was hidden in the sense that whenever a service thought it couldn't proceed, it would send out a message. E.g. Shop would say it had a completed Order. This Order would then be picked up by Payment and Fulfillment. Payment would allow a customer to pay and Fulfillment would have to wait because it needed paid Orders. So when Payment was done it would send out a PaymentReceived message that would allow Fulfillment to continue. This works but Greg argues that this allows only a single process and that the solution would be more flexible if we would have a process manager that delegates steps in the process to different services, waiting for them to complete.
That touches upon an aspect that wasn't implemented in our earlier solution: what happens if payment takes to long? In our first solution this would mean we would have a database with completed but unpaid orders. The problem of abandoned shopping carts could be solved by running a cleaning process that would send a message to customer support prompting them to call the customer, or just to get rid of the order. This is where our earlier solution starts to feel a bit constrained; what if we needed several services to find out what to do? Implementing process logic in a separate service seems to make sense, so this version of the code tries to do just that to find out what the consequences will be.
The process now looks like this:
So ShopManager starts a session and creates a Clerk. In the real world a Clerk would be a person who pushes your shopping cart around for you while you do the shopping, takes you to the payment terminal when you're done, delivers the cart to fulfillment so the contents can be shipped to you and finally brings you back to the exit and waves goodbye while you leave the parking lot (sounds like a great idea actually).
I won't describe all details here but highlight some of the key points below. To help understanding the process I would advise to start in the [scenarioTest sub project].
[Clerk] functions as a sort of container for all the state we need in the shopping process. This is comparable to a clerk in the real world who pushes your shopping cart around for you while carrying a clipboard that holds other information about you or the shopping process.
The process from the perspective of the clerk is easy to see in the [EventListener class].
It starts in [ClerkController] when the software receives a POST on /shop/session. That results in a StartShopping message being sent.
This message is picked up by the shop component, look for an EventListener class in the shop sub project. You can follow the flow by checking EventListener classes and calls to rabbitTemplate.convertAndSend() in each service.
One important consequence of this architecture is that we need to pass all data about the clerk around between the services and make sure all of it is send back to the ShopManager service each time a step is completed. In previous versions we got away with partial implementations of the domain in e.g. the payment or fulfillment services (using the double edged sword that is @JsonIgnoreProperties(ignoreUnknown = true)"), but now that isn't possible anymore because we're sending all of the Clerk data around. To make life easy for myself I just copied all classes in the domain package to each service. A particular service updates its part of the document and when its sub-process completes it sends all of the document back to ShopManager. I'll refactor this to get rid of the copied code in a later version.
The [ShopManager class] in the shopManager project keeps an eye on Clerks. Whenever a Clerk is created and sent on its way, ShopManager starts a session that will expire after a while. If the session expires and the process isn't done yet (the Clerk is left standing in the shop somewhere) ShopManager cleans up the mess. In this example it only cleans up its own mess, but in real life it would have to send clean up messages to each component involved in the process.
Centralizing the implementation of the process makes it easy to define what should happen in exceptional situations, so this is an advantage we gained from changing the architecture.
But more importantly it now becomes possible to change the process based on external properties like the type of customer or shop.
Plans and good intentions...
The picture below shows how far I've got up till now. Next I'll describe how to get rid of most of the domain classes that were necessary to process Clerk messages and I hope to find the time to study Greg Youngs ideas about messages and notifications. There's lots more to explore: I've introduced Docker to run the services which might be interesting in its own right or combined with a solution to run several versions of the software concurrently. Another interesting experiment would be to allow different processes based on characteristics of, e.g. the customer.