Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
I’d argue in most software that domain logic is written using Transaction Scripts. Transaction Scripts are single procedures that handle a request. While this can work fairly well in a lot of cases, it starts falling apart when you have a lot of domain complexity and/or a lot of various concerns. A sign you’ve gone too far is difficulty testing and wanting to call a transaction script from another transaction script. When this happens, what’s a solution? Encapsulating your domain logic and state changes within a Domain Model.
Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.
For this post, I’m going to use simplified food delivery as an example. You can think of this when you order food online or use a food delivery app. Your order requires a shipment where a delivery driver is going to pick up the food at the restaurant and deliver it to you.
The shipment consists of two stops, the pickup stop (restaurant) and the delivery stop (you’re location/home).
One key aspect is that a stop must go through a state transition. When the delivery driver is on their way to the stop, its status is “In Transit”. Once the delivery driver arrives at the stop it then moves to “Arrived”. Finally, after they either pick up the food or drop off the food, the stop moves to “Departed”.
Organizes business logic by procedures where each procedure handles a single request from the presentation.https://martinfowler.com/eaaCatalog/transactionScript.html
You can implement my food delivery example using transaction scripts. Each action (arrive, pick up, deliver) can all be implemented using their own transaction script.
Here’s an example of doing the Arrive action.
There isn’t much logic here, it’s just checking to see if the stop actually exists and if it’s not In Transit, it means we’ve already done the Arrive.
If you were using the Repository Pattern the end result is still the same. Yes, you’d be abstracting the data access with Entity Framework, but ultimately you’re still making the state change and validation in this handler.
So this is pretty straightforward. All good right? Well, what happens when we need to start adding more logic?
In the next example, I’ve added logic so that you cannot do an Arrive on a stop if you haven’t fully transitioned the previous stop. Meaning you can’t do an Arrive on the Delivery stop if you haven’t Departed the Pickup stop.
Alright, still not terrible, but hopefully, you can start seeing how over time you can slowly add more and more complexity. So why not add more for example!
I’m not going to add more logic but rather we’re going to publish an Event to a message broker. One single line at the bottom of the handler.
We may want to do this because we have a consumer for the event that will send a push notification to the mobile app of the customer who placed their order. This way they know that the delivery driver arrived at the restaurant and will be on their way to deliver the food soon.
Overgrown Transaction Script?
Have we gone too far by adding too much domain logic and different concerns in our transaction script? There are a few ways to tell if you have.
The first is testing. Do you find it difficult to test? If you find it difficult to write tests because you’re creating a lot of stubs, mocks, or fakes, this can be a pretty good indicator you’ve gone to far and the transaction script is doing too much.
The second is when you start having the need to have one transaction script call another transaction script. This is an issue because transaction scripts are a single unit and generally a transactional boundary, this can be problematic because you’d want the entire execution to be one atomic operation.
To illustrate this the following is the handler for doing a Pickup. This is when the delivery driver is picking up the food from the restaurant. This is done after the Arrive action.
The above example has a rule that you cannot do the Pickup action if you haven’t done the Arrive yet. But we now have to change this because the new requirement is that if the delivery driver forgot to do the Arrive action, just automatically do the Arrive action and then do the Delivery.
How do we automatically call the Arrive transaction script from the Pickup transaction script?
This is where a domain model becomes useful. It allows us to encapsulate the domain logic and state transitions (data) for a group of domain entities into a single unit. It becomes a consistency boundary.
The transaction scripts (handlers) are invoking an Aggregate (domain model) to make a state change rather than the transaction scripts (handlers) doing it themselves.
This means we can take all the domain logic and state changes out of the transaction scripts and put them into an Aggregate, specifically the Aggregate Root.
Take note of the Pickup method. The requirement of doing the Arrive if the Pickup has not been done yet was an issue with a transaction script. However, now we aren’t limited as the Arrive and Pickup live together within the Aggregate Root. So now we can check to see the current state and if the stop has not yet Arrived, we can call the Arrive() method.
Now the Transaction scripts are just facilitating getting an aggregate, calling the appropriate method on the aggregate, and then saving/persisting it.
Transaction Scripts or Domain Model
Should you always use a domain model? No.
Should you always use transaction scripts? No.
There is a balance that needs to be made. You can start with a transaction script if it’s very CRUD in nature without much logic. However, you must pay attention to complexity. The moment you start adding a significant amount of complexity or have too many concerns within a transaction script that make it difficult to test, or you want one transaction script to call another transaction script, you should think about adding a domain model that can help isolate the domain logic complexity.
Developer-level members of my YouTube channel or Patreon get access to the full source for any working demo application that I post on my blog or YouTube. Check out the YouTube Membership or Patreon for more info.