What is domain-driven design?
The problem DDD solves
You've built Rails apps. You've scaled them. You've designed systems that handle millions of requests. Sooner or later, though, every growing codebase hits the same wall: the code no longer reflects the business.
A User model has 80 methods. A controller creates enrollments, sends emails, calculates XP and validates passwords, all in one action. A developer asks "where does the enrollment logic live?" and the answer is "everywhere."
That isn't a scaling problem or a performance problem. It's a modeling problem: the software has drifted away from the business it represents.
Domain-Driven Design (DDD) is a set of principles, introduced by Eric Evans in his 2003 book, for structuring software so that the code mirrors the business domain. The code speaks the same language as the business, and the boundaries in the code match the boundaries in the organization. When the business says "enrollment", there is an Enrollment namespace in the code that owns that concept.
Ubiquitous language: the foundation
The most underrated idea in DDD is also the most important one: ubiquitous language.
Ubiquitous language means developers, product managers and domain experts all use the same terms, and those terms appear in the code itself: in class names, method names and module names, not in comments.
Take this LMS. The business talks about:
- Courses and lessons (what students learn)
- Enrollments (a student joining a course)
- Submissions (code a student sends for evaluation)
- Experience points (XP earned for completing work)
Now look at the codebase:
# The business says "Catalog" — the code says "Catalog"
Catalog::Course
Catalog::Lesson
# The business says "Enrollment" — the code says "Enrollment"
Enrollment::Enrollment
Enrollment::LessonCompletion
Enrollment::ExperiencePoint
# The business says "Execution" — the code says "Execution"
Execution::Submission
When a product manager says "a student enrolls in a course", a developer can point to Enrollment::StartCourseService and say "that's this operation." There's no translation layer, and no mental mapping from business ideas to arbitrary code structure.
At senior level, ubiquitous language is more than a naming convention. It keeps the code and the business aligned. When the code uses different words from the business, bugs hide in the gap between them. "User" in the identity context means login credentials. "User" in enrollment means a student with progress. Call both User without separating the contexts and you'll merge responsibilities that should stay apart, which is how User becomes the 80-method monster.
Strategic vs tactical DDD
DDD works on two levels, and teams often confuse them.
Strategic DDD is about the big picture:
- Where do you draw the boundaries between parts of the system?
- Which parts of the domain set the business apart from competitors?
- How do the parts of the system talk to each other?
Tactical DDD is about implementation patterns:
- Entities, value objects, aggregates
- Domain services, application services
- Repositories, factories
flowchart TB
subgraph Strategic["Strategic DDD"]
direction TB
StrategicDesc["Bounded Contexts, Context Maps, Core/Supporting/Generic"]
subgraph Tactical["Tactical DDD"]
TacticalDesc["Entities, Value Objects, Services, Aggregates"]
end
end
Most of the value comes from the strategic level. Drawing the right boundaries matters more than whether you use a repository pattern or call ActiveRecord directly. Shopify's large Rails monolith works because its boundaries are sound, not because every class follows tactical DDD patterns.
GitHub organizes teams around product areas (Actions, Codespaces, Copilot) with clear ownership. Basecamp keeps a monolith but splits it into areas with clear responsibilities.
The LMS domain: a living example
This LMS is built with DDD. This is how the domain breaks down:
| Domain | Namespace | Responsibility |
|---|---|---|
| Identity | Identity:: |
Who is the user? Authentication credentials, current session |
| Auth | Auth:: |
How do users prove who they are? Sign-in, sign-up, password policy |
| Catalog | Catalog:: |
What can be learned? Courses, lessons, challenges |
| Enrollment | Enrollment:: |
What has a student done? Progress, completions, XP |
| Execution | Execution:: |
What code did a student submit? Submissions, results |
| Admin | Admin:: |
How do administrators manage content? CRUD operations |
| Public | Public:: |
What does a visitor see? Marketing, landing pages |
| Community | Community:: |
What are learners saying? Comment threads on lessons |
| Partners | Partners:: |
Who sponsors or hires? Sponsorships, partner inquiries, referrals, certificate sharing |
Each domain owns its logic. Auth::PasswordPolicy defines the password rules, and the model doesn't repeat them. Enrollment::StartCourseService handles enrollment, so the controller doesn't. Enrollment::UserGamificationQuery calculates XP, so no user.total_xp method leaks across domains.
That is DDD in practice: clear ownership, clear language, clear boundaries.
Why DDD matters more as systems grow
Small apps don't need DDD. A weekend project with 5 models and 3 controllers works fine with standard Rails conventions. The models are small, the logic is obvious, and everyone understands everything.
Systems grow, though, and they grow in two directions at once:
- Complexity: more business rules, more edge cases, more integrations
- Team size: more developers, more opinions, more changes at the same time
Without DDD, growth creates a tangle where everything depends on everything. Changing the enrollment logic breaks notifications because they share models. Adding a new course type means editing 14 files across 6 directories.
With DDD, growth adds rather than tangles. Each domain evolves on its own. The enrollment team changes enrollment code, and the catalog team changes catalog code. When they need to interact, they go through defined interfaces instead of reaching into each other's internals.
Most of the cost of software is understanding it six months later, not writing it. DDD optimizes for that understanding.
Common mistakes
Starting with tactical patterns before you understand the domain
Entities, repositories and factories come first, and the domain comes later. You end up with neatly structured code that models the wrong thing: an elegant repository doesn't help if your bounded contexts are wrong.
Instead, map the domain with the people who know it. Find the language, the boundaries and the core ideas first. Tactical patterns come later.
Treating DDD as all or nothing
Teams apply DDD everywhere, simple CRUD screens included, and drown in ceremony.
Instead, apply strategic DDD broadly, because good boundaries help everything. Apply tactical DDD selectively, only in the core domain, where the business logic is complex.
Equating DDD with microservices
DDD is about logical boundaries, not deployment boundaries. A Rails monolith with well-defined modules, like this LMS, is a perfectly good home for DDD.
Instead, use DDD inside your monolith. Extracting a microservice is a separate decision, for when you need to deploy a part independently.
Summary
| Concept | What to remember |
|---|---|
| Domain-Driven Design | Software should model the business domain: same language, same boundaries |
| Ubiquitous language | Developers and business experts use the same terms, and the code uses them too |
| Strategic DDD | Drawing boundaries between parts of the system; most of the value is here |
| Tactical DDD | Implementation patterns (entities, value objects, services); apply selectively |
| The LMS domains | Identity, Auth, Catalog, Enrollment, Execution, Admin, Public, Community, Partners; each owns its logic |
| The growth problem | Without boundaries, complexity turns into a tangle where everything depends on everything |
| DDD is not microservices | DDD defines logical boundaries; deployment boundaries are a separate decision |
Keep going with lesson 2
Bounded contexts & context maps
Explains bounded contexts, the signals that mark their boundaries, and context mapping patterns across the nine LMS contexts.
A free account also gives you the quizzes, your progress and notes, and the discussion.