DDD in Rust: How to Write Domain Code Without Pain and Macros

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Domain-Driven Design in Rust Projects: When Types Become Your Allies



Have you ever written a Rust application where business logic drowned in a sea of structs, traits, and macros? That familiar feeling when, looking at the code, you can't immediately tell what a particular function does in the context of the domain? If so — welcome to the club. Domain-Driven Design (DDD) aims to solve this problem, and Rust with its type system is the perfect playground for implementing DDD principles.



In this article, we won't retell Evans' book. Instead, I will show you how to apply Domain-Driven Design in Rust projects with real-world examples. You will learn how to make domain code clean, safe, and understandable using wrapper types, modular architecture, and expressive structs. Let's go!



Why Rust and DDD Are a Perfect Match



Domain-Driven Design requires strict isolation of domain logic from infrastructure. Rust provides powerful tools for this:



  • Type system — allows modeling business rules at the compiler level. An error? The compiler won't let you make it.
  • Ownership and borrowing — guarantee that aggregates and entities won't be accidentally modified in an unexpected place.
  • Module system — clearly separates the domain layer, infrastructure, and application.


But there are pitfalls too. Rust does not forgive "fat" objects with lots of mutable state. Therefore, DDD in Rust requires a different approach to modeling than, say, in Java or C#.



Key Idea: Make Invalid States Unrepresentable



In DDD, we strive to encode business rules into the data structure itself. In Rust, this becomes a mantra: "If a state is invalid — make it impossible to create."



Instead of writing:



struct User {    name: String,    email: String,    age: u8,}


We create types that guarantee correctness at compile time:



pub struct Email(String);pub struct Age(u8);pub struct UserName(String);

pub struct User { name: UserName, email: Email, age: Age,}


Seems like just wrappers? In fact, this is the first step towards DDD in Rust, where types become documentation and guardians of invariants.



Modeling the Domain: Value Object and Entity



The central concepts of DDD are Entity and Value Object. In Rust, they are implemented differently.



Value Object in Rust: Immutability and Equality by Value



A Value Object has no identity. Two objects are equal if their fields are equal. In Rust, this fits perfectly with structs using #[derive(PartialEq)] and immutable fields.



use std::fmt;

#[derive(Debug, Clone, PartialEq)]pub struct Money { amount: i64, currency: Currency,}

#[derive(Debug, Clone, PartialEq)]pub enum Currency { USD, EUR, RUB,}

impl Money { pub fn new(amount: i64, currency: Currency) -> Self { // Business rule: money cannot be negative assert!(amount >= 0, "Amount must be non-negative"); Self { amount, currency } }

pub fn add(&self, other: &Money) -> Result<Self, String> { if self.currency != other.currency { return Err("Cannot add different currencies".into()); } Ok(Self::new(self.amount + other.amount, self.currency.clone())) }}


Notice: Money is immutable. Any operation returns a new object. This aligns with the philosophy of DDD in Rust: no setters, only methods that create new instances.



Entity: Identity

Blogs

Book Recommendations