Skip to main content

Own your Domain before it ends up owning you!

··2130 words·10 mins
Driven by Domains The Art of Architecture Philosophy

Everybody and their mother has heard of Domain Driven Design. People love throwing the term around in architecture meetings to sound smart. But what does it actually mean? And more importantly, how do we actually put it into practice without overcomplicating things?

Stripped of the hand-waving, DDD is nothing more or less than the translation of business processes into their technical representation while sustaining the real-world terminology, rules and relations. What’s left is answering why we should even bother and how we can actually do it in a simple and reproducible way.

Get in loser, we’re going domaining!

Why give a f**k about the domain
#

Let’s get one thing straight: complexity will never go away. As developers, our main job is exactly dealing with that complexity. So, can we please stop talking about “eliminating” or “getting rid of” it? That’s not a thing just a dream. What we actually need is the right approach to tame and control it.

The domain is the set of rules that govern that part of reality that we are trying to abstract into a piece of functioning software. Those rules exist to stay clear of illegal states (the kind where already deleted users are the only ones that can approve a password recovery). And the second those rules are not enforced the system they were designed to hold together starts coming apart at the seams. Quietly at first. Then all at once.

What we tend to get wrong there: code need not be old to be legacy. You can write straight legacy on a Tuesday afternoon. What legacy actually is is just code you and your colleagues are out of touch with. Put differently that’s code where the rules baked into it are not or no longer obvious, documented and traceable, just completely lost in (technical) translation (just go ask Bill Murray). So, the further you drift from the domain, the more legacy your code is, meaning DDD serves us as a bulwark against turning legacy.

Bottom line is that putting extra time into the domain is no kooky affair. It is to lock in the correct functioning of our systems by making illegal states unrepresentable in the first place. That’s the whole game.

Revisiting Plato in his cave
#

To be honest, I find the “Domain Driven Design” terminology a tad misleading. The word “design” implies agency, a creative process conscious choices being made, creating something on a blank canvas. But that’s far from what’s happening or what should be happening. Think of it this way: Plato had this theory about prisoners chained in a cave, only ever seeing shadows of real things flickering on the wall. Solely based on the shadows, the prisoners can draw conclusions on what the actual objects casting the shadows are. There is no way to verify with the real thing, however.

my man plato livin’ in a cave

This is the same way as we are confronted with the domain. It clearly exists upfront and is lived through the actual business processes. It ain’t up to you to draft how an Order look like and how it relates to Fulfilments. That is pre-established by reality and you only get to deduce it by asking questions and reflecting on things or put differently: watching the shadows.

In a similar way, the actually valuable part of our jobs as Engineers is really not writing the code and being purely technical, but it’s much more centered around humans and the reality of business. That is, cause it’s about mediating. Between the domain and the code. Between the people who know what should happen and the machines that need to be told in all detail possible. They call it Domain Driven Design, but really it is just paying attention and actively trying to understand.

do what kanye says

What about that fancy blue book?
#

Eric Evans’ seminal book on DDD is a curious specimen. Everybody knows it. Many own a copy. Fewer have actually read it. And hardly anybody can sum it up for you simple enough to understand. Now, myself is part of the read it ages ago and don’t remember much details, but without knocking it, here’s what stuck with me as general ideas and how me think about it now:

Ubiquitous Language
#

This one is about being very precise about language use throughout the business, meaning the same terminology that is used in everyday life and by domain experts should be the one ending up in the code. But again, language on its own is not enough to guarantee a shared understanding. The same term can evoke very different concepts and ideas in the heads of different people.

But, being precise about one’s language, the “using the same terminology” thing doesn’t sit too well with me. Meaning, Ubiquitous Language shouldn’t really focus on naming and terminology alone, but rather on shared concepts. The words are only the surface, a signpost, the real work is making sure that there is a shared understanding of the concepts beneath throughout technology and functional teams.

shared concepts

Putting Things together that belong together
#

This one sounds so damn obvious and easy, but it’s another subjectivity trap. What does “belong together” mean anyway? Well, depends who you ask: everything that touches the same database tables? Everything sharing the same lifecycle? Everything synced from the same outside source? All services? Everything that needs to deploy together?

All fair, but none will lead to module boundaries aligning with your domain. We need things that belong together in the domain, meaning being non-separable and dependent in the context of business transactions. Getting that right is genuinely hard, figuring it out with the first try is virtually impossible, making it the trickiest part about DDD. Anyone claiming to have a clean rule for it is selling you something.

pineapple belong on pizza?

Bounded Contexts
#

Bounded contexts seem to pick a fight with the biggest dogma in our industry: Don’t Repeat Yourself. Two decades of dev culture have indoctrinated us to treat any line of duplication as a moral failure. The common misconception if you ask me:

Duplicating code is completely fine. Duplicating business logic is trouble!

This mindset is crucial to pull off bounded contexts. Take a pizza. In an ordering context it’s a SKU, a price, and a list of toppings. In the kitchen it’s a recipe with ingredients and stuff. In inventory it’s just about the stock. In delivery it’s solely about target arrival time. In accounting it’s a tax-categorized line item. Five contexts, five very different models for the same physical pizza. And quite possibly the same or at least to some degree shared database tables underneath.

Instead of shoving it all into a do-it-all god Pizza object shared across the different domains (and while at it, diluting their boundaries), each domain needs its own independent model, completely catering to each bounded context’s requirements. Storage can absolutely be shared at the bottom, but the models on top need to stay independent. There is a fine line between duplication and independence. It’s what lets the accounting part evolve their workflow without influencing checkout, the kitchen or delivery.

But now how to find what belongs together into the same bounded context?

Think transactions, not entities
#

We need to start top-down, looking at our system as different vertical slices from incoming request until eventual persistence. But what does that mean?

Look at the holistic transactions first, with transaction meaning a single execution unit triggered by something incoming. The transaction. The business event. What it means, what it touches, what rules apply, what the possible outcomes are.

Once you have that, the rest falls out almost naturally. You will see what the bounded contexts are. You will see where the seams should be. You will see which pieces of state need to be consistent together and which ones absolutely do not.

Horizontal separation of responsibilities does not add any real value or encapsulation. Dependencies criss-cross with everything reachable from everywhere, blurring the dependency graph as a whole. Your processes do not know services, repositories or controllers.

Think transactions, not entities.

The vertical slices also lead to a chance and opportunity of encapsulating said parts of the domain.

Everything is an API
#

Now having vertical and separate slices of the system, there is also something to protect and hide. Meaning, we need to talk encapsulation, because:

Don’t leave the door wide open if you don’t want strangers walking in to eat and comment on your home-cooked food.

goldie locks api

An important part of properly encapsulating our individual subdomains is the Model. It’s historically a low-key controversy what the model is and how to deal with it. One approach is to implement dedicated model classes that live between Entities and DTOs, but that is often ditched to avoid a shit ton of mapping between different structures. The model is the closest thing to the actual domain object, neither a 1-to-1 representation of the underlying database tables nor of the API payloads on the way out.

We want to use the model to filter and funnel data manipulations, especially those that belong together and those that aren’t allowed at all. State manipulations are a great example. They’re often tied to other data changes or come with specific lifecycle rules. Take a Pizza: it shouldn’t expose setState() to let any consumer drop it into whatever state they fancy. It should expose bake(), which leads to the controlled, correct internal state update.

model api 4 data

The same logic applies to changes that need to happen together. Adding a topping changes both the topping list and the price. Don’t let the caller set both separately and risk forgetting one. Expose withTopping() and let the model handle the price change internally. The caller never gets the chance to forget. This ties straight back to the basic idea of making illegal states unrepresentable.

Aside: This is also where the idea of the perfect method in the OOP context comes from: no param, no problem. The perfect method comes with zero arguments, cause that means the underlying object is fully self-sufficient.

This often reduces the service layer to a very thin transaction wrapper, but that’s perfectly fine. Those thousand-line Service classes full of feature envy (calling the setter of an object with the getter result of another object) won’t be missed.

Patterns of DDD
#

Looking at specific patterns for cross-module communication in DDD, there are two worth calling out:

Shared kernel (it’s a trap)
#

The shared kernel is relatively simple: extract shared events (used to trigger and react to actions in other modules) and a limited number of core components into its own module, the shared kernel, and depend on it from both sides. This looks super clean on a diagram.

shared kernel trap

The catch: we cannot trace the actual flow of transactions and data by looking at the dependencies between different subdomains, cause dependencies all just go to the shared kernel. As an extra, removing a module can lead to unforeseen issues as there won’t be a compile time error to alert us right away.

The conscious dependency
#

A different move (and the one preferred by me) is being very explicit about the cross-module dependencies. But to pull it off, we need the right mindset and be wary and aware of the state and ramifications of dependencies throughout the systems at all times. This way, we can actively steer how dependencies play out instead of letting them just happen, which is the classic way of ending up with a tangled mess. I call it Dependency Aware Development (DAD), cause no buzz without a buzzword. The whole thing can still be event based, but respective events live in the subdomain they belong to and listening to that event always comes with the respective dependency on the outer subdomain. Exactly what we want.

conscious dependency

The Bottom Line
#

So what’s the takeaway? Domain-Driven-Design (or better: Deducing Da Domain) boils down to correctly separating business contexts and having an easier time getting correct system behavior as a result. Along the way, we make illegal states unrepresentable to achieve this.

How well we can pull it off is all about the right mindset: encapsulating properly, cause everything is an API the moment something else can call it, be it a package, class or even method. Be aware of your dependencies and steer them deliberately, that’s the whole Dependency Aware Development thing. And actual transactions matter more than entities, so put on your Requirements Engineering hat before you start typing. That might not feel like “real coding”, but surely adds real value.

Finally, not all pizza are created equal and it’s fine and often just necessary to have different pizzas buzzing around your system when in different contexts. Take back ownership of your domain and stop being a victim forced to be reactive to your system.