What is domain-driven design?

Free preview 7 min to read

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:

  1. Complexity: more business rules, more edge cases, more integrations
  2. 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
Lesson 1 of 7 · free

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.

Type to start searching...