Why DDD is Not About Boring Diagrams, But About Living Architecture
\nWhen a C++ project outgrows the size of a "toy" application, developers often encounter chaos: business logic is smeared across controllers, the database dictates class structure, and newcomers are afraid to touch legacy code. Domain-Driven Design (DDD) is not a set of abstract rules, but a practical tool that helps tame complexity. In this article, we'll break down how to implement DDD in C++ using concrete code examples, and why it will save your project from becoming a "big ball of mud."
\n\nWhat is DDD and Why Does a C++ Developer Need It?
\nDomain-Driven Design is an approach where the primary focus is on the domain model, not on technical details. In C++, this is especially relevant: the language provides powerful tools (templates, RAII, smart pointers), but without proper architecture, the project turns into a monolith where changing one feature breaks everything.
\nDDD offers two levels:
\n- \n
- Strategic Modeling: dividing the system into contexts (Bounded Contexts) and configuring their interaction. \n
- Tactical Modeling: building blocks — Entity, Value Object, Aggregate, Repository, Domain Service. \n
We will focus on tactical modeling, as it provides concrete patterns for C++ code.
\n\nLayered Architecture: The Foundation of DDD in C++
\nDDD implies a clear separation into layers. A typical structure for a C++ project looks like this:
\n- \n
- Domain Layer (core): entities, value objects, aggregates, domain services. No dependencies on frameworks or external libraries. \n
- Application Layer: use cases, orchestration. Depends only on Domain. \n
- Infrastructure Layer: implementation of repositories (SQL, Redis, files), work with external APIs. Implements interfaces from Domain. \n
- Presentation Layer: user interface (console, REST API, GUI). \n
This is not a strict rule, but a recommendation. The main point is that dependencies point inward: outer layers can depend on inner layers, but not vice versa.
\n\nExample: How Not to Do It
\n// ❌ Bad: business logic mixed with SQL\nclass UserController {\n void createUser(const std::string& name) {\n // SQL query right here\n db.execute("INSERT INTO users (name) VALUES (?)", name);\n // Validation logic also here\n if (name.empty()) throw std::runtime_error("Name required");\n }\n};\n\n\nExample: DDD Style
\n// ✅ Good: clear separation\nclass User : public Entity<User, int> {\n std::string name_;\npublic:\n explicit User(int id, std::string name) : Entity(id), name_(std::move(name)) {\n if (name_.empty()) throw std::invalid_argument("User name cannot be empty");\n }\n const std::string& name() const { return name_; }\n};\n\n// Repository interface (Domain)\nclass IUserRepository {\npublic:\n virtual ~IUserRepository() = default;\n virtual void save(const User& user) = 0;\n};\n\n// Implementation (Infrastructure)\nclass SqlUserRepository : public IUserRepository {\n void save(const User& user) override {\n // Only SQL, no business logic\n }\n};\n\n\nDDD Building Blocks in C++
\n\nEntity
\nThis is an object with a unique identifier that persists over time. In C++, it's convenient to create a base template class:
\ntemplate<typename T, typename IdType>\nclass Entity {\n IdType id_;\npublic:\n explicit Entity(IdType id) : id_(std::move(id)) {}\n IdType id() const { return id_; }\n bool operator==(const Entity& other) const { return id_ == other.id_; }\n