Domain-Driven Design: How Top Teams Build Software That Actually Works

As software developers/ software architects, we often come across the question, “Why should I use your application?” Most of the enterprise-grade applications that we work on, whether greenfield or brownfield, have already been worked on or are already present.

So why should a client use your application 🤔? What problems does it solve that other similar applications don’t solve, and how?

Creating an application that answers the above questions often starts with recognising domain-specific problems requiring technical solutions.
Working on medium to large software systems, you realise that the real challenge is not solving complex algorithms, it’s orchestrating a system of many moving parts.

As the codebase grows and microservices multiply, designing solutions that integrate seamlessly, leverage existing components, and extend the system thoughtfully becomes increasingly difficult.

Domain-driven design is a philosophy that provides principles and patterns to address problems faced while developing complex domain models.

Umm… what is a domain?

Every software program relates to some activity or interest of its user. The subject area to which the user applies the program is the domain of the software.

And what are models??

A model is a simplified interpretation of reality that abstracts the aspects relevant to solving the problem at hand and ignores the extraneous details. It is the teams agreed upon way of structuring domain knowledge and distinguishing the elements of most interest.

And hence, a domain model is a rigorously organised and selective abstraction of domain knowledge.

But software teams understand software and may not always have an idea of the domain they are working on 😵‍💫

Knowledge Crunching

For a software team to understand the domain, they need help from the people who are experts in the given domain, a.k.a Domain Experts.

But… domain experts are not always aware of how complex their mental processes are, as, in the course of their work, they navigate all these rules, reconcile contradictions, and fill in gaps with common sense. Software can’t do this. It is through knowledge crunching in close collaboration with software experts that the rules are clarified, fleshed out, reconciled or placed out of scope.

Knowledge crunching is the practice of continuously exploring, questioning, and refining one’s understanding of the domain till they arrive at a simple view that makes sense of the mass. Many models are tried, rejected or transformed. Success comes in an emerging set of abstract concepts that makes sense of all the details. These models are never perfect – they evolve.

Ubiquitous Language

Domain experts have a limited understanding of the technical jargon of software development, but they use the jargon of their field (probably in various flavours). Developers, on the other hand, may understand the system and discuss it in descriptive functional terms, devoid of the meaning carried by the expert’s language.

On a project without a common language, developers and domain experts have to translate for each other. Translations blunt communication and make knowledge crunching anaemic 🥶.

The domain model can provide the backbone for that common language, while connecting the team communication to software implementation. That language can be ubiquitous in the team’s work.

Let’s take an example of a cab service like Uber. The domain expert might tell the following points:

  • Rider requests a ride
  • The system finds a nearby driver
  • The driver accepts or rejects
  • Ride gets confirmed
  • Fare is calculated

For this, a developer might write this entire logic inside a method like:

assignDriver(rideId) {...}

While the method may be technically correct and functionally complete, it fails to capture the domain’s true intent and language.

When looking at a software design, we should actively go back and forth between the viewpoints of a developer/architect and a domain expert.

If we read the points given to us by the domain expert again, we would easily be able to categorise them into:

And this shift in mindset even reflects in our code, as our lines of code map to something a domain expert will actually say, like:

Ride.request(...)

DriverMatchingService.matchDriver(ride)

Ride.offerTo(driver)

Driver.acceptRide(ride)

Ride.confirm(driver)

The vocabulary of Ubiquitous Language includes the names of classes and prominent operations.

Bounded Context

As a project grows, the same terms might mean different things in different parts of the system.

For example, the term customer in the above cab service example might mean different things to different parts of the system:

  • For Trip Management, a customer is someone who requests a cab or is taking a trip now.
  • For Payment Management, a customer is someone responsible for making a financial transaction.
  • For Support, a customer is someone with an issue or complaint.
    … and so on.

Domain Driven Design solves this problem by breaking the domain into independent parts called Bounded Context.

Characteristics of Bounded Context:

  • Every bounded context is represented by its own unique domain model.
  • A domain model built for a bounded context is applicable only within its boundaries.
  • Each bounded context has its own ubiquitous language, meaningful only within that bounded context.

The building blocks of Model-driven Design

Model-Driven Design provides a framework for the realisation of systems using the Domain-Driven Design approach.
Domain objects/ Business objects are the foundational elements for defining the concepts in a domain model.

Defining objects that represent domain concepts may seem intuitive at first, but it comes with significant challenges.

One of the considerations to keep in mind is:

Does an object represent something with continuity and identity that can be tracked through different states or across even different implementations? Or is it an attribute that describes the state of something else?

This, in fact, is the basic distinction between an entity and a value object.

Entity

An object whose defining characteristic is its identity rather than its attributes is called an Entity.
These objects define a thread of identity that runs through time and often across distinct representations.

Sometimes,

  • Such an object must be matched with another object even though the attributes differ.
  • An object must be distinguished from other objects, even though they might have the same attribute.

But then, how do we know that the two objects represent the same conceptual Entity 🤔?

The definition of identity emerges from the model. Sometimes, certain data attributes or a combination of attributes can be guaranteed or simply constrained to be unique within the system, providing a unique key for the entity.

When no such unique key is possible, a common solution is to attach to each instance a symbol (a number or a string) that is unique within the class.

Once this ID symbol is created and stored as an attribute of the entity, it must be designated as immutable.

Continuing with the cab service example, a few of the entities in a cab service domain, based on an initial high-level analysis, include:

Value Object

Value objects, unlike entity objects have no conceptual identity in the bounded context. The attributes or behaviour of the value objects do not map directly to the core concepts in the bounded context.

In other words, an object that represents a descriptive aspect of the domain with no conceptual identity is called a value object. These values are instantiated to represent the elements of the design that we care about, only for what they are and not who or which they are.

A few of the value objects in a cab service domain, based on an initial high-level analysis, include:

Often, a value object might be shared between multiple entities (each with a pointer to the same instance of the value object) with no change in their behaviour or identity. This behaviour is considered correct until it is possible to mutate the value object. To protect against this, for a value object to be shared safely, it must be immutable.

If the value of an attribute changes, you can use a different value object rather than modifying the existing one.

Service

Some concepts from the domain are not natural to model as objects. Forcing the required domain functionality to be the responsibility of an entity or value distorts the definition of a model-based object.

A service is an operation offered as an interface that stands alone in the model, without encapsulating state as entities and value objects do.

A service is a verb rather than a noun.

A good service has the following characteristics:

  • The operation relates to a domain concept that is not naturally part of an entity or value object.
  • The operation is stateless; that is, the service does not hold or remember any internal data between calls.

A few of the services in a cab service domain, based on an initial high-level analysis, include:

Conclusion

Now that we’ve built a solid foundation in the fundamentals of Domain-Driven Design 🥳, it’s time to go a layer deeper.

In the upcoming posts, we’ll explore how these concepts play out in real-world systems, uncover practical patterns, and tackle the nuances that make or break a good domain model.