An Introduction to Domain-Driven Design - DDD w/ TypeScriptDomain-Driven Design
Have you ever worked on a codebase where it felt like "the more code I add, the more complex it gets"?
Have you ever wondered, "how do you organize business logic anyways"?
Or have you ever been in the situation where you're nervous to add new code to an existing codebase in the fear that you'll break something else in a completely different part of the code somewhere?
What about enterprise companies? How are they doing it? Their codebases must be massive. How do they get anything done? How do they manage that complexity?
How are able they able to break off large bodies of code, assign them to teams, and then integrate all the teams together?
I've wondered all of this while coding on a 3 year old Node.js app with a line count pushing ~150K+.
I came across Domain-Driven Design when I realized I needed it the most.
Quick history about me
In 2017, I started working on an application called Univjobs, a marketplace for Canadian students and recent-grads to find co-op jobs, gigs and entry-level jobs and internships.
The MVP was pretty simple. Students could sign up, create their profile and apply to jobs. Employers could sign up, post jobs, browse students and invite them to apply to the jobs they've posted.
Since 2017, we've iterated many times, adjusting and encorporating features based on feedback from students and employers such as job recommendations, interviews and an Applicant Tracking System.
Eventually, the codebase had became so large that adding new features on top of it took nearly 3x the amount of time it would have taken when I first started.
Lack of encapsulation and object-oriented design were to blame.
I had an Anemic Domain Model.
It was at this point I started to seek out solutions to the problem.
About Domain-Driven Design
Domain-Driven Design is an approach to software development that aims to provide a framework for creating software to match the mental model of the problem domain we're addressing.
Initially conceptualized by Eric Evans who wrote the bible of DDD (famously known as the Blue Book), it's primary technical benefits are that it enables you to write expressive, rich and encapsulated software that's both testable and maintainable.
Generally speaking, it enables us to do this through the use of a Layered Architecture, domain-modeling building blocks, and a Ubiquitous Language.
The Ubiquitous Language is a common language that best describes the domain model concepts. It must be learned by actually spending time talking with the domain experts. This language, once agreed upon, is the way to connect what the software looks like to what actually occurs in the real world.
If we're building an app that helps recruiters hire talent, we need to spend some time understanding the domain language and processes that exist from the recruiters' perspective.
That means actually talking to the domain experts.
Layered Architecture & Design Principles + Patterns
Domain-Driven Design requires fundemental knowledge of software design patterns and principles. It works well in an Agile context and places importance on delivering the simplest thing possible first (YAGNI), then improving on it's design iteratively.
The challenge is: you need to know these fundemental design principles and patterns in order to design well the first time. It's a lot harder to do DDD well if we make a mess.
In order to go fast, we must go well.
In order to do DDD well, we need to keep the SOLID principles in mind, organize a central domain layer at the core of our Layered Architecture, and implement interface adapters to persistence, web and external technologies. We don't want these things to sully our domain model.
We want to keep them at a distance so that we can isolate our domain and keep our unit tests fast.
I studied Java in high-school and University. Like a lot of my peers, I didn't really LOVE Java a whole lot because:
a) We hated seeing red lines in the compiler all the time. This was scary for a 1st year University student learning how to program and
The community was much more interesting than the Java community to me as a musician and a gamer (at the time).
Like many others, we learned how to build Node.js backends through YouTube, Scotch.io, Udemy and Udacity courses. This was also the extent to which a large number of developers from my generation learned about software design.
Model + view + controller.
This works great for a large number of RESTful web backends, but for applications where the problem domain is complex, we need to break down the "model" part even further.
To do that, we use the building blocks of DDD.
Very briefly, these are the main technical artifacts involved in implementing DDD.
These are objects that we care to uniquely identify. They have a lifecycle where they can be created, updated, persisted, retrieved from persistence, archived and deleted.
Entities are compared by their unique identifier (usually a UUID or Primary Key of some sort).
Value objects have no identity. They belong as attributes of Entities. Think
Name being a Value Object on a
They're compared by their structrual equality.
These are a collection of entities are that bound together by an aggregate root. The aggregate root is the thing that we refer to for lookups. No members from within the aggregate boundary can be referred to directly from anything external to the aggregate. This is how the aggregate maintains consistency. This is how we model relationship tables and tags.
This is where we locate domain logic that doesn't belong to any one object conceptually.
We use repositories in order to retrieve domain objects from persistence technologies. Using software design principles like the Liskov Subsitution Principle and a layered architecture, we can design this in a way so that we can easily make architecture decisions to switch between an in-memory repository for testing, a MySQL implementation for today, and a MongoDB based implementation 2 years from now.
We'll want to create domain objects in many different ways. We map to domain objects using a factory that operates on raw sql rows, raw json, or the Active Record that's returned from your ORM tool (like Sequelize or TypeORM).
Domain events are simply objects that define some sort of event that occurs in the domain that domain experts care about.
- write testable business-layer logic
- spend less time fixing bugs
- watch a codebase actually improve over time as code gets added to it rather than degrade
- create long-lasting software implementations of complex domains
Domain modeling is time-consuming up front and it's a technique that needs to be learned.
Because it involves a lot of encapsulation and isolation of the domain model, it can take some time to accomplish.
I'm really glad you're here and you're reading this.
If you're Junior Developer or getting started in the world of software architecture and design, I think you're on the right track.
Domain-Driven Design has introduced me to a world of software architecture, patterns and principles that I might not have naturally started learning until much later.
From my own experience, it's largely a "you don't know it until you need it" kind of thing where:
a) you realize you need to model a complex domain and it seems daunting so you try to find the right methology to approach it or
b) your codebase has become so large that it's hard to add new features without breaking new things, so you seek the solution to that problem or
c) someone more experienced than you brings it to your attention
d) you read my article and you realized you have an anemic domain model and you don't wish to have one.
The thing about Domain modeling is that it does take a little bit of time to start to get comfortable with. It can be a bit awkward to get accustomed to organizing your code this way, but when you start to reap the benefits of DDD, I think you'll naturally prefer to organize your backend code this way over the Anemic Domain Model and Transaction Script approach.
More in this series so far..