Domain-Driven Design for javascript developers

A 10 minutes introduction to grasp the whys and hows of DDD for your project

Pierre Criulanscy
Spotlight On JavaScript
11 min readJan 11, 2019

--

Be warned, you’re going to need a lot of sticky notes! Photo by Ferenc Horvath on Unsplash

Domain-Driven Design at a glance

Domain-Driven Design is a methodology first brought up to date by Eric Evans in Domain-Driven Design: Tackling Complexity in the Heart of Software in 2003.
It was firstly a methodology aiming at guiding software engineers to implement complex systems standing on the shoulders of the object-oriented programming giants and based upon a very good understanding of the domain by talking to domain experts.

If today the DDD approach has refocused more on domain modeling, the ins and outs remain the same but the implementation is not tied to the object-oriented approach anymore. A more functional approach or even a mixed one is totally an option and a natural fit for javascript. And with the increasing complexity of javascript projects, why not give Domain-Driven Design a try?

What can you expect from this post?

Domain-Driven Design is a very large topic, books with hundreds of pages have been written on the subject. My goal is not to make you an expert on Domain-Driven Design in 10 minutes, I am by no means a DDD expert either.
I want you to understand why DDD can be useful for your project, how to architecture a DDD project, how you can implement some concepts in javascript and what is the data flow you should strive for.
This post focuses on javascript, however, to better understand the main parts of DDD it’s simpler to place ourselves in the object-oriented software engineer’s shoes.

The Ubiquitous Language

The ubiquitous language (UL) is one of the pillars of DDD. In a nutshell, the UL describes precisely what objects, behaviors, use cases are in your project so everyone can understand it, from the project owners with no technical knowledge to the newly onboarded developer.
It serves the very purpose of DDD: sharing a common understanding of the project to facilitate communication between all involved parties. This UL is defined and refined as your knowledge of the domain goes.

Domain experts should object to terms or structures that are awkward or inadequate to convey domain understanding; developers should watch for ambiguity or inconsistency that will trip up design. — Eric Evans

When applying Domain-Driven Design, you want to focus on the most critical part of your application first: the one that is delivering value for your organization. You also don’t want to struggle with technical choices at the beginning because that can slow you down while you’re not totally certain that specifications aren’t going to change drastically. It does happen really often when a project has just started, especially in the javascript landscape. That’s why DDD can help us think strategically about the main parts of our application and implement them with good tactics.

Strategic & tactical patterns

Photo by rawpixel on Unsplash

If you play chess (or any other strategy game), you can draw an analogy between strategic patterns and the global strategy you’re aiming at winning the game. That strategy may change over time, with additional knowledge gathered along the way, and is here to guide your next moves. Tactical patterns are those tactical moves you can do to play a specific chess situation to pursue your strategy.

In DDD, strategic patterns are large-grained patterns used to distill the domain knowledge and act as the global strategy to guide the implementation :

  • Domain: a domain is what your organization does. If you organize live music shows, your domain is “live music shows organization”, if you sell shoes online, your domain is “online shoes retailing”, you nailed it. When referring to the specific part of what your organization does, we use the term Core Domain whereas when talking about less important areas of your business we use the term Subdomain.
  • Subdomain: a subdomain is not the main area of expertise of your organization thus making it less critical than your Core Domain but still not optional if you want to provide a full-fledged solution.
  • Bounded Context: The ubiquitous language makes sense in a specific context. An “account” means different things whether you see it in a banking context (a bank account) or in a user management context (a user account).
    In DDD we call this kind of context a bounded context. A bounded context defines a clear conceptual boundary around a whole application or a part of it. Outside the bounded context, the same word may (and certainly does) means something totally different. Bounded context forms the second pillar of DDD.
Each white shape delimits a domain, a core domain or a subdomain and is contained in its own bounded context (the dotted boundary). Domain and bounded-context have a 1 to 1 relationship, it’s what we should strive for in a greenfield project.

Armed with a better understanding of your domain, it’s time to write some code guided by the tactical patterns. In Domain-Driven Design the design is the code and the code is the design.

Tactical patterns are not specifically tied to DDD and are just good programming practices focused on a clear separation between business logic and infrastructure concerns. To achieve this kind of separation of concerns, we need good architecture. But what is good architecture?

The architecture

As stated by Uncle Bob, when designing our architecture we should aim for an architecture :

  • independent of Frameworks
  • testable
  • independent of UI
  • independent of Database
  • independent of any external agency

To do so, we need to separate our application into layers with different responsibilities. We tend to represent those layers concentrically :

3 concentric layers architecture, dependencies point inward

The inner layer is your business logic, the application layer orchestrates this business logic in response to clients requests and the infrastructure layer contains concrete implementations of the code dealing with the database, web-services, etc.

There are many architectures following those guidelines: Onion Architecture, Ports & Adapters Architecture (or Hexagonal Architecture), or the Clean Architecture to name a few.

What they mostly have in common is that they need to follow the Dependency Rule :

This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in an inner circle. That includes, functions, classes. variables, or any other named software entity. — Uncle Bob

We can use the Dependency Inversion Principle to declare in the inner layers abstractions that will be implemented (and potentially injected at runtime) in the outer layers. So inner and outer layers depend on abstractions instead of implementation details.

Let’s now see the main tactical patterns at our disposal in the OOP world (in pseudocode).

  • Entity: entities are mutable objects with identifiers. Their state is modified in response to some business logic. Two entities are equal if they have the same identifier. A User with id = 42 will semantically be the same if we retrieve this User few hours later. An entity can keep a reference to another entity through its identifier, not by having a real in-memory reference to the actual entity object. Entities belong in the domain layer.
  • Value Object: as stated in the name, value objects represent values as properties, are immutable, and hold methods that implement business logic.User entity can have a fullName value held by a FullName value object: new FullName(firstName, lastName). Two value objects can be considered equal if they have the same values for their properties: new FullName('john', 'smith').equals(new FullName('john', 'smith')) // => true. Value objects belong in the domain layer.
  • Aggregate: aggregates are objects aggregating entities and value objects together. An aggregate is responsible for keeping business invariants inviolated at any given time and thus represents a transactional boundary. Let’s take a chat application as an example. Users send messages, and users can like messages. An obvious invariant states that a user can’t like a deleted message. This invariant could be violated if a user likes a message when in the meantime the message’s owner is deleting it. This use case could be made possible if we were deleting a Message aggregate while saving a Like aggregate referencing this message. By creating a Message aggregate holding a list of the Like entities, we can no longer violate this rule because, in both “deletion” and “like” case, we act on the same Message in the database. The transaction started when the message’s owner deletes the message would prevent the transaction from “liking” the message to be started. Thus, only the aggregates can be retrieved from and saved to the database. Aggregates belong in the domain layer.
  • Domain Event: a domain event is an event indicating that something relevant has happened in the system. Domain events belong in the domain layer.
  • Repository: a repository is a facade for retrieving and querying aggregates from the database, used to avoid tight coupling between this querying/saving logic and the technical implementation. Repository interfaces belong in the domain layer and their concrete implementation in the infrastructure layer.
  • Domain Service: sometimes you need to write domain logic that doesn’t naturally fit in an Entity or a Value Object . This is a good indication that you may need a domain service to implement this logic. You should name this service according to your ubiquitous language. However, you might be tempted to put all the logic in this kind of services, thus creating an anemic domain model, as we’re going to see it later. Domain services logic and interfaces belong in the domain layer and their concrete implementation in the infrastructure layer.
  • Application Service: an application service handles a specific application use case by retrieving needed aggregates from the database, delegates business logic to those aggregates so they can mutate their internal state according to the use case, then save back the aggregate in the database (or eventually return to the client any data needed to display some view). Application services belong in the application layer.

Rich Domain Model vs Anemic Domain Model

The goal of those patterns is to help us achieve a rich domain model. A rich domain model is a domain where all the business logic is well encapsulated in the entities and the value objects. It’s a pillar of the oriented-object paradigm: data and behaviors acting on the data should be encapsulated together as a single unit.

If your entities are “data holder” structure, with only getters and setters, it means that the logic is implemented elsewhere, in domain services for example. Those domain services are then responsible for getting the data from entities through getters and updating those entities via their setters. This uncontrolled mutability opens the way to a lot of problems because you are not sure anymore if the entity has been modified or not elsewhere. Your model is now anemic. It’s a strong anti-pattern in OOP.

However, in functional programming, an anemic domain model is what we should strive for because the separation of data and behaviors is the very purpose of functional programming, to favor composition of reusable units. The fact that FP favors pure functions and immutability totally alleviates the need for classes as an encapsulation mechanism. Modules are way more appropriate. First-class functions, immutability, modules, that sounds like very good fits for a javascript implementation!

Applying Domain-Driven Design concepts to javascript

Although ES6 defines sugar for writing classes in a more OOP way, it still is what it is: sugar over prototypal inheritance. Since functions are first-class citizens in javascript, we’re going to apply a more functional approach to DDD than an object-oriented one. We’re going to favor :

  • modules and factory functions as an encapsulation mechanism over classes
  • immutability and pure functions over mutable classes
  • higher-order functions for dependency inversion and dependency injection over interfaces (well…because there is no interface in javascript)

This logic is implemented in the different aggregates/entities/value objects/domain services. We’re going to use modules and factory functions as encapsulation mechanisms. Let’s say we have a “Foo” aggregate type, we want to implement its data structure and the behaviors acting on the data, and the repository method for getting and saving a “Foo” aggregate in the database. Since we are in the domain layer, we must only define abstractions for the repository methods. Here what might be the folder structure :

├── projectName/
| ├── domain/
| | ├── foo/
| | | ├── behaviors.js
| | | ├── data.js
| | | ├── getFooOfId.js
| | | ├── saveFoo.js
  • the projectName/domain/foo module groups everything related to our “Foo” aggregate
  • the projectName/domain/foo/data.js file contains the factory function used to create an immutable “Foo” data structure :
  • the projectName/domain/foo/behaviors.js file contains the behaviors function acting on FooData. These are pure functions that take a FooData with some additional parameters needed to apply the behavior and return a new FooData immutable structure. Let’s say that our “Foo” aggregate has a really useful business rule stating that when thebar value is set then the foobaz value should be set to 42 if the bar value contains a “?” :
A very useful behavior! In a real project, this function should be named according to the ubiquitous language, not with a “setter” logic
  • the projectName/domain/foo/getFooOfId.js is the higher-order function that we must use to build a concrete getFooOfId implementation. We are in javascript so nothing really forces us to use this method but it acts as a documentation of the domain needs :

The Application layer

The application layer depends on the domain layer and has its infrastructural dependencies injected at runtime (repositories, domain services, etc.). The application layer contains application services: i.e: use case handlers. These handlers are responsible for retrieving the needed aggregates to this use case and for delegating them the responsibility to handle the use case since the behaviors (aka the business logic) belong inside the aggregates.

The code might look like this :

The folders structure is now :

├── projectName/
| ├── domain/
| | ├── foo/
| | | ├── behaviors.js
| | | ├── data.js
| | | ├── getFooOfId.js
| | | ├── saveFoo.js
| ├── application/
| | ├── updateBarUseCaseHandler.js

The infrastructure layer

As we can see in the above code, our updateBarUseCaseHandler is a higher-order function accepting a getFooOfI function and asaveFoo function as dependencies. These functions will be injected at runtime, be we need to create them in the first place! Let’s implement them as functions acting on an in-memory database for the sake of this example, based on the functions defined in the domain layer :

The folders structure is now the following:

├── projectName/
| ├── domain/
| | ├── foo/
| | | ├── behaviors.js
| | | ├── data.js
| | | ├── getFooOfId.js
| | | ├── saveFoo.js
| ├── application/
| | ├── updateBarUseCaseHandler.js
| ├── infrastructure /
| | ├── inMemory.js

The Composition Root

To tie it all together and inject the correct dependencies at runtime, we need a way to “instantiate” our whole project with the given dependencies. This is what we called a composition root. We need to compose those objects together as close as possible to the application’s entry point. Here, our application is not very useful and has only one use case, by “instantiating” our project I mean having the possibility to send commands to it, let’s expose a MyVeryUsefulProject function that will do that, in a index.js file for example :

├── projectName/
| ├── domain/
| | ├── foo/
| | | ├── behaviors.js
| | | ├── data.js
| | | ├── getFooOfId.js
| | | ├── saveFoo.js
| ├── application/
| | ├── updateBarUseCaseHandler.js
| ├── infrastructure /
| | ├── inMemory.js
├── index.js

It’s a very very contrived example, but we can now write a unit test asserting that the foo object was correctly saved by using our inMemory functions in the projectName/__tests__/updatingBar.test.js file :

Conclusion

This post was an introduction to Domain-Driven Design, and how we can apply it to javascript. The examples were very contrived on purpose, so stay tuned for the next posts where I’m going to talk, step-by-step, about a real-life implementation of the CQRS architecture, event-sourcing, event storming, unit and integration tests, and how all of this can be applied in a real-time web application!

Keep in mind that you might not need to follow the DDD path for your project. If your project is essentially a CRUD project, DDD is overkill. But if your project suffers from concurrency issues, complex logic scattered everywhere, it might be a good sign to give DDD a go!

--

--