Roman Weis
CodeX
Published in
13 min readMay 14, 2022

--

Photo by Pixabay: https://www.pexels.com/photo/close-up-photo-of-cute-sleeping-cat-416160/

Let’s build Business Software — an alternative approach to the standard DDD implementation

Be it a web application or code running on the chip of your door bell, a simplified view of every program in the world consists of these three steps:

1. Take some data as input

2. Run some logic operation on the data input

3. Produce some data output

We can think of this process as a pipeline where we put some data in at one end and we get some data out on the other end.

Real world applications are complex systems build upon an enormous amount of such pipelines. The complexity is breath taking and a testimony of great mental abilities of humanity. At the same time a single human alone cannot oversee the full complexity. Abstractions and patterns are introduced to help us to think and communicate with each other about software systems. Programming languages are the most important tools to translate our ideas into machine readable code, but they are not enough.

On top of every programming language we use different programming paradigms to structure and write a program, such as procedural, functional, object-oriented programing and others.

Many different patterns for code structure and implementation were found and written down by programmers as best practices for the future generation. But there is a problem. The great amount of patterns, best practices, guidelines and frameworks has become increasingly complex itself. This defeats the initial purpose to reduce complexity of a software program.

The increasing complexity has a direct and measurable impact on the business side. Prolonged development times for seemlingly simple features and updates consume much of our time and budget which prevents us from working on innovative business ideas.

Business value is the reason

When developing software there are basically three main goals that we should optimize for:

- create business value

- keep the program code as simple as possible

- be compliant with security, performance and other non-functional requirements

But business value is the main reason why we write software in the first place. Anything else are just tools to help us get there quick and safe. Any decision is ultimatly driven by business. One of the books that influenced my thinking about software is the “blue book” Domain-driven design by Eric Evans. It’s a manifesto for business value in software. Programming languages, platforms, patterns, methodologies and anything else are just the means to translate our ideas into working code.

The implementation details

In his book Eric Evans offers an implementation model which is known as the technical DDD. There you’ll find the aggregate root, the value object, domain service and so on. I’ve spent quite some time studying this model, visiting DDD workshops and applying it in real world applications and several projects. In the following I am going to demonstrate an alternative implementation model which evolved from my own experience applying the technical DDD patterns.

Everything is a service

A very common source of confusion is the question whether a certain functionality should be implemented as a method of an aggregate root or as a domain service. The general recommendation is to try to make it a method because the responsibility of the aggregate root is to create this transactional boundary and make sure that the business rules are consistent within this boundry.

Unfortunatly as the project matures and we learn more and more about the domain, we begin to realize that our initial transactional boundaries are by far not sufficent. To satisfy a new complicated business rule we need to interact with other aggregate roots which introduces an unwanted and potentially dangerous dependency.

Enter the “Domain Service”. This is the thing that we begin to use to manage the interaction between different aggregate roots while pretending that our aggregate roots are still in charge of the game. But unfortunatly this is not the case. The most interesting and important things now happen in our domain services. The ultimate endgame is that our aggregate roots become “anemic” and all of our business logic resides in the domain services.

So what can we do? Go back to the books, read again, study, visit another workshop and finally realise that we are too dumb to understand how to make it right? That’s certainly an option. But we also could try to find a pragmatic solution for the implementation details and still use the strategic patterns of DDD.

Let’s think for a moment: if the juicy business logic tends to end up in the domain services then maybe it’s just the place where it belongs to? Yes, the domain services are ugly beasts, but at the end of the day they are just those pipelines that take some input, apply some business rules and create some output. Wait, that sounds like pure functions, should we use functional programming to implement our domain rules? Let’s not rush it too much into yet another extreme before we understand better what we are dealing with.

We need to take a step back and think about what we really want to achieve. That’s what I did. In the following I am going to describe the most important patterns that helped me to establish a straight forward, consistent methodology to implement business rules.

Task based commands — behavior first

A task based command is an object where the name of the command describes the intent of an application user to change the state of the application. For example “Book selected Room” describes a command with the intent, you guessed it, to book the selected Room. The object could have the following shape:

class BookSelectedRoom {
selectedRoomId: string;
startTime: Date;
endTime: Date;
}

The command describes the intent and provides some context in form of properties. We can implement the business rules to fullfil the given task and run the command. When the command execution was succesful, parts of the application state should be changed e.g. a meeting was created, invitation emails and reminders were created and so on.

In order for the command to succeed we want that important business rules are satisfied e.g. we might want to restrict a room to one meeting at the same time. This logic typically resides in the so called command handler, but I think the better option is to keep things together that belong together and just add a method “handle” to our BookSelectedRoom class. With that we also get rid of the extra class BookSelectedRoomHandler and reduce a bit the complexity of our code.

But how should the handle method ensure all this complex business invariants without any information about the application state?

In a command handler the dependency problem is usually solved with dependency injection where the command handler gets instatiated by the DI container and receives other services and repositories as dependencies. But this explicit statement of actually internal implementation details is a problem for testing.

A good rule of thumb is that our tests should test the public api and should not change when implementation details change. Imagine our command handler has a dependency X. In our tests we mock or instatiate this dependency X and pass it to the testee. A month later we decide to replace the dependecy X with dependency Y or add a dependency Z. Now we need to fix all the tests and setup the correct mocks or stubs, although the business rules or requirements did not change.

In summary we have two reasons not to use command handlers:

  1. increased code complexity
  2. stability of our test suite.

To tackle the code complexity problem we choose to implement the handle method directly on our command. But how to solve the dependency problem? Enter the “Application State”.

The application state

A thought experiment: imagine if you had all the computational power and all the storage at your finger tips where every database commit happens instantly, where network failures are not existent, bandwith is unlimited and latency is equal to zero. How would you write your computer programms?

Many of the design and architectural patterns that we know emerged from the need to optimize the performance of the system. In a perfect world we could simply take the full state of a huge and complex application, change the string of that tiny little object somewhere in the corner of the system and save back the whole application state without affecting any other users. In the real world we have to make sure that we read only the required object and that we don’t lock the whole application state when saving our changes.

We cannot bend the laws of physics, but the idea to have the application state available at our finger tips is still appealing. Could we pass a reference to our application state as a whole? Then our handle method could decide what parts of the state it needs to satisfy the complex business invariants. The interface of such an application state could look like that:

interface State {
getById<T>(id: string): T where T is Entity;
findAll<T>(query: QueryExpression)
add(entity: Entity);
remove(entity: Entity);
resolveService<S>();
saveChanges();
}

The above implementation looks a bit like a generic repository, but it has some significant differences. Firstly, it is not bound to a single Entity type. Secondly, it represents the application state which is not only data, but also registered services and some application specific methods. It’s more like a facade for the universe of our app.

Unit of work pattern

The state that we reference in memory is obviously not the full state which is on physical disks. We use just a portion of the state in memory and lazy load the data that we need for the task at hand.

Also when we add a new object to the state it does not get committed instantly to disk. Instead we accumulate our changes in our in-memory state and after we are finished we try to commit all changes at once and if it fails we throw away our changes and maybe try again later.

This is the working principle of the so called “unit of work” pattern. The main advantage is that we can work in-memory and be sure that our changes are committed all or none. We can chain our command handlers in memory and be sure that no inconsistent changes are written to disk.

The handle method

Let’s now implement our handle method and add some context.

interface Entity {
id: string;
}
class Room : Entity {
lastBookedOn: Date;
}
class RoomBooking : Entity {
roomId: string
startDate: Date;
endDate: Date;
isBlockedOn(date: Date) {
return date > endDate || date < startDate;
}
}
class ClockService {
getTime(): {
return new Date();
}
}
class RoomBookedEvent {
id: string;
roomBookingId: string;
isHandled: boolean;
}
class BookSelectedRoom {
selectedRoomId: string;
startTime: Date;
endTime: Date;
handle(State state) {
// find future room bookings
var roomBookings = state
.findAll<RoomBooking>(r =>
r.roomId == selectedRoomId &&
r.endDate > startDate);
// check if room is already booked
if (roomBookings
.Any(r =>
r.isBlockedOn(startTime) ||
r.isBlockedOn(endTime))) {
throw new BusinessError(“room is already booked”);
}
// add a new room booking
var roomBooking = new RoomBooking() {
id: Rand.newId(),
roomId: selectedRoomid,
startDate: startDate,
endDate: endDate,
}
state.add(roomBooking)
// update the last booked date for optimistic concurrency
var clockService = state.ResolveService<ClockService>()
var room = state.getById<Room>(selectedRoomId);
room.lastBookedOn = clockService.getTime();

// add a domain event to the state
state.add(new RoomBookedEvent(){
id: Rand.newId(),
isHandled: boolean;
roomBookingId: roomBooking.id
})
}
}

The handle method uses the application state to select all relevant future bookings for the room. Then it checks whether any room booking already exists for the selected time period. If there is already a room booking we throw a BusinessError. We want to treat business errors and technical errors differently. A business error might just trigger an alert on the user interface, while a technical error should notify the dev team that something unexpected happened. If there is no room booking for this time period yet, we just create a new one. And then also other interesting things happen.

Dependency injection

First we resolve the ClockService. Obviously it only works, if we are using some sort of dependency injection under the hood. The implementation of the application state needs to forward our request to the DI container which returns the ClockService. We are using this dependecy without an explicit declaration in the constructor or elsewhere. If you have been using DI for years like me, it might physically hurt you when you see this, but I think it’s actually not that bad.

The ClockService is a dependency, but it is not part of the public API. We do not put the pressure on a regular user to think about this ClockService and how to pass the correct instance. It’s an implementation detail that should not litter the public API. Also we might now just exchange the ClockService for some UtcClockService for example. Then we only need to update our testing DI container and our tests should still work, we won’t have to go through all the tests and update all instances.

Optimistic concurrency

The second interesting part is the update of the lastBookedOn property on the Room object. It seems really useless since we are not enforcing any rule here. But we need it for concurrency reasons. Imagine that another user is trying to book the room at the same time for the same time slot. Both users read the list of existing room bookings, both find that there is no room booking yet, both create a new room booking. There is no other check that prevents both room bookings to be written to disk after the command is finished which would lead to double bookings.

To prevent this concurrency issue both users need to read and change an existing object. When both try to save their changes on the same object then optimistic concurrency kicks in. Depending on the database technology that we are using the implementation of optimistic concurrency will be different, but the general idea is that:

The database keeps an internal version or hash of the objects. When we load an object from the database into memory we also keep the latest version number/hash and when we try to update the object we also send this version to the database. The database checks whether the version changed in the meantime and will reject the write operation if it did.

Domain events

In the last step we are storing a new domain event that tells the system what happened: a room was booked. The domain event is obviously not handled yet and it holds a reference to the room booking id. Notice that we won’t try to implement a handle method on the domain event because there might be many or no handlers at all and the implementation of such a method would become quickly a mess. Instead we will create new commands that will handle this event. For example we could think of a HandleRoomBooking command in the invoicing context that would create an invoice for the room booking.

Domain event handling

You may have noticed that there is only one isHandled boolean flag while we would expect to have more than one event handlers. This flag is actually only used to ensure that the domain event is submitted to a message bus. The message bus has to make sure that all subscribers of the event will receive their copy of the event. The subscribers are again commands with a handle method and the domain event copy as property. The next question is: how do we actually make sure that the domain event is submitted to the message bus.

Publishing domain events

One option is to publish the domain event directly on the message bus, after we saved our changes. This does introduce the problem of a two phase commit. The problem is in general not easy to solve. The main issue is that the message bus connection could be gone after we saved our changes and as a result or domain event will be never published. Also it is difficult to just retry the publishing of the domain event since we would need to re-run the command.

A second option works only if the underlying database technology has a build-in messsage queue that notifies subscribers about changes. The downside is that we would need an always-on service that subscribes to this changes. In a serverless world this is not a very satisfying solution. In addition our architecture gets a little bit more complex since we have to implement, deploy and monitor an addtional service.

The option that I finally settled with is a variant of the first one. Instead of submitting the real domain event to the message bus, I would first publish a “check event” on the message bus which will trigger a check function later. If the publishing of the “check event” fails we just need to re-run the publishing. Also there is a good chance that the publishing is done anyways with the next command. The check function will collect all pending domain events and submit them to the message bus. The main advantage is that the process is repeatable and we can trigger the check function at any time to settle all unhandled domain events.

Thin application layer

Now that we have all the building blocks we need to stitch it all together to make it actually work. The commands can be triggered via simple http post requests. We can create a simple commands controller that receives the command as json object in the body. The body gets deserialized and the command object instance is created. Now we need to resolve the application state from our DI container and run the handle command. At this point we could add a retry logic, logging etc. or add more methods on the command to handle permissions logic, validate command properties and more.

The best part is that we need to implement the application layer just once and use it for any further command.

Back to business

We took a step back and defined our new building blocks for the implementation of the domain. Does it solve the initial problem of the Domain Services? From my perspective the answer is: yes.

One problem with the technical implementation as suggested in the original DDD book is the focus on object-orientation. It makes perfect sense if you look at it from the context of the time in which the book was written, but during all my years working with different teams and learning about DDD, I came to the conclusion that the technical DDD implementation model is either too hard to understand or not simple enough to be battle proof.

At the end we have to be pragmatic in order to get things done and my approach is now to focus 100% on the behavior — the commands — of the domain and concentrate the logic there instead of trying to find the perfect aggregate root for the concept of a “car” or a “bank account” or a “room”. It seems that object-orientation is hard to get right and often ends up in spagetthi code.

On the other side we have this simple imperative approach with scoped commands that handle single use cases with task based commands. The intention of the code is easy to understand just by reading the names of the commands classes. The code inside the handle method can be a mix of procedural, functional and object-oriented paradigms. Those are just implementation details which can change, hopefully without breaking our automated tests.

This was a very brief overview of the design patterns that I’ve been using over the last 2 years in production of real world applications. In this time I was able to reduce the complexity of technical details and focus more on the domain which enabled me to build and manage a complex online booking system.

I hope you enjoyed the read, cheers!

Roman

--

--

Roman Weis
CodeX
Writer for

Software Engineer, SaaS Enterpreneur. Founder of https://www.stamy.io. Writing about Software Engineering and other brain teasers.