What DDD Is — Starting From the Microsoft Ecosystem and the Repository Abstraction
During project development I encountered DDD (Domain-Driven Design) for the first time. I had previously worked with .NET MVC and also learned Go, and I realized many concepts are actually related to things I already knew — especially the Repository Pattern. This post documents how I used my existing knowledge to understand DDD.
The Request-Handler Approach
MVC stands for Model-View-Controller, three roles:
- Model — data and business logic
- View — presentation (HTML, JSON response, etc.)
- Controller — receives user actions, calls the Model to process them, passes the result to the View for rendering
|
|
Conceptually, the Controller is a middleman — it does not make logic decisions; it only coordinates the Model and the View.
Practical Architecture in the Microsoft Ecosystem
The textbook version of MVC has three layers, but in practice .NET MVC splits the Model further. Cramming business logic and database access together inside the Model becomes hard to maintain once the codebase grows, so the real-world split looks like this:
|
|
In an API scenario the View is just a JSON response, so it does not need much extra handling.
Take “activating a brand” as an example:
|
|
The Brand entity has only properties and no methods — it is just a data container. All the decision logic (“archived cannot activate”) lives in the Service. This kind of entity is called an anemic entity — it has data but no behavior.
Repository Pattern
The core idea behind the Repository Pattern:
Use an interface to abstract “how data is accessed,” so the Service does not need to know whether the underlying store is SQL Server, PostgreSQL, or in-memory.
|
|
Wire the interface to the implementation via DI (Dependency Injection):
|
|
The Service depends only on the IBrandRepository interface, not on the concrete SQL implementation. When you want to swap databases or write tests, just swap the implementation.
Once you understand this, DDD is not difficult — the Repository Pattern is already part of DDD.
What the Domain-Centric Method Adds
DDD stands for Domain-Driven Design. At first it sounded abstract, but once I grasped it, the core idea boils down to one thing:
Move business logic from the Service back onto the Entity itself.
Same “activate a brand” scenario, written the DDD way:
|
|
|
|
Side-by-side comparison:
| .NET MVC | DDD | |
|---|---|---|
| Entity | properties only (anemic) | properties + methods (rich) |
| Service | all decision logic here | only load → call → save |
| Repository | interface + implementation | same concept |
The Repository part is completely unchanged. The only difference is where the business rule lives.
Why Move It?
When 10 different Services all operate on Brand, the .NET MVC way scatters the “archived cannot activate” rule across those Services — it is easy for someone to miss one.
With DDD, that rule lives inside Brand.activate(), so no matter who calls it the check is enforced. The Entity protects its own state.
Core Concepts of the Domain-Centric Method
Identifiable Objects
Has a unique ID, has behavior, and changes over time.
|
|
The difference from a .NET Entity is the addition of methods. Two Entities are compared by ID, not by property values.
Value-Based Objects
No ID; compared by “value.” Immutable.
|
|
Analogy: two hundred-dollar bills — you do not care which specific bill, only the amount. That is a Value Object — compare by value, not by identity.
The closest equivalent in .NET is C# 9’s record, which defaults to value-based equality.
Consistency Clusters
This concept does not exist in .NET MVC, and it took me a while to understand it.
An Aggregate is a group of things that must always be accessed together, with a root entity as the entry point. Think of a Post and its Comment entries:
|
|
Rules:
- Outside code can only touch the root (
Post), notCommentdirectly - The entire aggregate is loaded and saved together
- Different aggregates reference each other only by ID, never by holding the other’s object
The third point is the opposite of .NET EF Core’s navigation property. EF Core lets you write post.Author.Name to directly access the User object; DDD does not do this — it stores only author_id, and if you need the User you query it yourself.
The reason: if Post directly holds a User object, changing User means worrying about whether Post breaks. Using IDs to decouple means each aggregate manages only itself.
Domain Scope Demarcation
How do you decide what belongs in the same module?
DDD’s answer is to look at “language.” When the same word means different things in different scenarios, those are separate contexts.
Take “User” — for social features it means “a person with a follow list and profile”; for the wallet it means “an account with a balance and transaction history.” Although both are called User, the concerns are completely different, so the Users module and the Ledger module are two separate Bounded Contexts.
How our project splits them:
| Context | Concern |
|---|---|
| Users | identity, profile, follow, curation |
| Brand | brand data, Shopify integration |
| Ledger | wallet, transactions, commission |
| Affiliate | affiliate marketing, commission rate settings |
| Community | posts, comments |
How the Golang Ecosystem Handles It
When learning Go I was curious: does the Go community use DDD?
The answer is yes, but it is uncommon and the practice is looser. The Go community values simplicity and dislikes multi-layer abstractions, so a full DDD layering is rarely seen in Go projects. The standard practice is still handler → service → repository, similar to .NET MVC.
That said, a few DDD concepts come naturally in Go:
Go’s interfaces are implicit (no implements keyword), so defining a Repository contract is intuitive:
|
|
Entity with behavior uses a method on the struct:
|
|
Python’s Protocol (structural typing) works like Go’s implicit interface — you do not declare “I implement such-and-such interface”; if the method signatures match, it counts as implemented. This is quite different from C#, where you must write class Foo : IFoo explicitly.
Why Not Stick With the Three-Tier Pattern
I already know the Repository Pattern, so why bother with DDD?
Because as the project grew larger, we ran into several problems that the .NET MVC approach could not handle well:
- Services keep growing — all logic lives in Services; a single BrandService can be hundreds of lines, and changing one thing requires reading the entire file
- Rules scattered everywhere — the same business rule is written in multiple Services; fixing one and forgetting the others
- Modules tangled together — UserService imports BrandService, BrandService imports WalletService — a spider web
- No clear boundaries — wanting to split modules but not knowing where to cut
DDD’s solutions:
- Problem 1 → entities carry their own logic, services become thin
- Problem 2 → rules are written only on the entity, single source of truth
- Problem 3 → cross-module wiring goes only through the presentation tier, no direct imports
- Problem 4 → Bounded Contexts provide a criterion for where to cut modules
Summary
| .NET MVC concept | DDD equivalent | What differs |
|---|---|---|
| Entity (property only) | DDD Entity | DDD entities have behavior |
| Repository Pattern | DDD Repository | Nearly the same; DDD is stricter (only returns the aggregate root) |
| Service | Application Service | DDD services are thinner — only load → call → save |
| Controller | Presentation (route) | Same concept |
| DI Container (Startup.cs) | Composition Root (providers.py) | Same concept, different location |
| No direct counterpart | Value Object | Immutable, compare by value |
| No direct counterpart | Aggregate | A group of things that must always be accessed together |
| No direct counterpart | Bounded Context | Use “language” to draw module boundaries |
The next post will document the pitfalls I encountered in the actual project: drawing the wrong aggregate boundary, designing a double-entry ledger, and using an event system for cross-module communication.
References
- Domain-Driven Design Reference — Eric Evans
- Martin Fowler — AnemicDomainModel
- Martin Fowler — BoundedContext
- Martin Fowler — DDD Aggregate
- Martin Fowler — ValueObject
- Martin Fowler — Repository
- Microsoft — ASP.NET MVC Pattern
- Microsoft — Repository Pattern
- Python dataclasses — frozen
- Python typing — Protocol
- Go — Effective Go (Interfaces)