Skip to content

Domain-Driven Design in Rust Type-Safe

"Make Illegal States Unrepresentable" — Dùng Type System để enforce business rules

Triết lý DDD trong Rust

Rust có Type System mạnh nhất trong các ngôn ngữ mainstream. Thay vì validate data tại runtime (như Java/Python), ta encode business rules trực tiếp vào types.

┌─────────────────────────────────────────────────────────────────────┐
│              RUNTIME VALIDATION vs COMPILE-TIME TYPES               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Traditional (Java/Python):                                         │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │  String email = input;                                         ││
│  │  if (!isValidEmail(email)) throw new Error();  // RUNTIME      ││
│  │  // Compiler không biết email đã validated                     ││
│  └─────────────────────────────────────────────────────────────────┘│
│                                                                     │
│  Rust DDD:                                                          │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │  let email: Email = Email::parse(input)?;  // COMPILE-TIME     ││
│  │  // Nếu có Email, CHẮC CHẮN nó valid                           ││
│  │  // Compiler BIẾT và ENFORCE                                   ││
│  └─────────────────────────────────────────────────────────────────┘│
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

1. Newtype Pattern for Validation

Newtype = Wrapper type đảm bảo data đã được validate.

Ví dụ: Email Value Object

rust
use std::fmt;

/// Email đã được validate - Không thể tạo Email invalid
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(String);  // Private inner field!

impl Email {
    /// Parse và validate email. Trả về Err nếu invalid.
    pub fn parse(s: &str) -> Result<Self, EmailError> {
        // RFC 5322 simplified validation
        if s.contains('@') && s.len() >= 5 {
            Ok(Email(s.to_lowercase()))
        } else {
            Err(EmailError::InvalidFormat)
        }
    }
    
    /// Borrow inner string (read-only)
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

// Không implement From<String> để buộc đi qua parse()!

#[derive(Debug, thiserror::Error)]
pub enum EmailError {
    #[error("Invalid email format")]
    InvalidFormat,
}

Sử dụng

rust
fn register_user(email_input: String) -> Result<User, Error> {
    // ✅ Buộc validate tại boundary
    let email = Email::parse(&email_input)?;
    
    // Từ đây trở đi, email CHẮC CHẮN valid
    // Không cần validate lại ở DB layer, notification layer, etc.
    let user = User::new(email, ...);
    
    Ok(user)
}

Thêm Newtypes phổ biến

rust
/// Non-empty string (tên người dùng, title, etc.)
#[derive(Debug, Clone)]
pub struct NonEmptyString(String);

impl NonEmptyString {
    pub fn new(s: String) -> Result<Self, ValidationError> {
        if s.trim().is_empty() {
            Err(ValidationError::Empty)
        } else {
            Ok(Self(s))
        }
    }
}

/// Positive integer (quantity, age, etc.)
#[derive(Debug, Clone, Copy)]
pub struct PositiveInt(u32);

impl PositiveInt {
    pub fn new(n: u32) -> Result<Self, ValidationError> {
        if n == 0 {
            Err(ValidationError::Zero)
        } else {
            Ok(Self(n))
        }
    }
}

/// Money amount (cents để tránh floating point)
#[derive(Debug, Clone, Copy)]
pub struct Money {
    cents: i64,
    currency: Currency,
}

2. Type State Pattern

Encode trạng thái của object vào type system. Compiler ngăn chặn invalid transitions.

Ví dụ: Order Lifecycle

rust
// Marker types (Zero-sized)
pub struct Draft;
pub struct Submitted;
pub struct Paid;
pub struct Shipped;
pub struct Cancelled;

// Order generic over State
pub struct Order<State> {
    id: OrderId,
    items: Vec<OrderItem>,
    total: Money,
    _state: std::marker::PhantomData<State>,
}

impl Order<Draft> {
    pub fn new() -> Self {
        Order {
            id: OrderId::new(),
            items: vec![],
            total: Money::zero(),
            _state: PhantomData,
        }
    }
    
    pub fn add_item(&mut self, item: OrderItem) {
        self.items.push(item);
        self.total = self.total + item.price;
    }
    
    // Transition: Draft → Submitted
    pub fn submit(self) -> Result<Order<Submitted>, OrderError> {
        if self.items.is_empty() {
            return Err(OrderError::EmptyOrder);
        }
        Ok(Order {
            id: self.id,
            items: self.items,
            total: self.total,
            _state: PhantomData,
        })
    }
}

impl Order<Submitted> {
    // Transition: Submitted → Paid
    pub fn mark_paid(self, payment: Payment) -> Order<Paid> {
        Order {
            id: self.id,
            items: self.items,
            total: self.total,
            _state: PhantomData,
        }
    }
    
    // Transition: Submitted → Cancelled
    pub fn cancel(self, reason: String) -> Order<Cancelled> {
        // ...
    }
}

impl Order<Paid> {
    // Transition: Paid → Shipped
    pub fn ship(self, tracking: TrackingNumber) -> Order<Shipped> {
        // ...
    }
}

Usage: Compiler Enforces Valid Transitions

rust
fn process_order() {
    let order = Order::<Draft>::new();
    order.add_item(item1);
    
    let submitted = order.submit()?;
    
    // ❌ COMPILE ERROR: Order<Submitted> không có method add_item
    // submitted.add_item(item2);
    
    // ❌ COMPILE ERROR: Order<Submitted> không có method ship
    // submitted.ship(tracking);
    
    // ✅ OK: Valid transition
    let paid = submitted.mark_paid(payment);
    let shipped = paid.ship(tracking);
}

3. Algebraic Data Types cho Business Logic

Dùng enum để model exclusive states.

Ví dụ: Payment Status

rust
pub enum PaymentStatus {
    Pending,
    Processing {
        started_at: DateTime<Utc>,
        gateway_id: String,
    },
    Completed {
        completed_at: DateTime<Utc>,
        transaction_id: TransactionId,
    },
    Failed {
        failed_at: DateTime<Utc>,
        reason: PaymentFailureReason,
        retries: u8,
    },
    Refunded {
        refunded_at: DateTime<Utc>,
        amount: Money,
    },
}

pub enum PaymentFailureReason {
    InsufficientFunds,
    CardDeclined,
    NetworkError,
    FraudDetected,
}

Pattern Matching: Exhaustive Handling

rust
fn handle_payment(status: PaymentStatus) -> Action {
    match status {
        PaymentStatus::Pending => Action::WaitForGateway,
        PaymentStatus::Processing { started_at, .. } => {
            if Utc::now() - started_at > Duration::minutes(5) {
                Action::Timeout
            } else {
                Action::Wait
            }
        }
        PaymentStatus::Completed { transaction_id, .. } => {
            Action::NotifySuccess(transaction_id)
        }
        PaymentStatus::Failed { reason, retries, .. } => {
            if retries < 3 && is_retryable(&reason) {
                Action::Retry
            } else {
                Action::NotifyFailure(reason)
            }
        }
        PaymentStatus::Refunded { amount, .. } => {
            Action::NotifyRefund(amount)
        }
        // ❌ COMPILE ERROR nếu thiếu case!
    }
}

4. Domain Events

Events immutable, được type hóa đầy đủ.

rust
#[derive(Debug, Clone)]
pub enum DomainEvent {
    UserRegistered {
        user_id: UserId,
        email: Email,
        registered_at: DateTime<Utc>,
    },
    OrderPlaced {
        order_id: OrderId,
        user_id: UserId,
        total: Money,
    },
    PaymentReceived {
        order_id: OrderId,
        payment_id: PaymentId,
        amount: Money,
    },
}

// Event handler type-safe
pub trait EventHandler {
    fn handle(&self, event: DomainEvent) -> Result<(), EventError>;
}

5. Bảng Tóm tắt DDD Patterns

PatternRust ImplementationBenefit
Value ObjectNewtype (struct Email(String))Validation at construction
EntityStruct with IDIdentity-based equality
AggregateStruct containing entitiesConsistency boundary
Type StateGenerics with marker typesCompile-time state machine
Domain EventEnum variantsExhaustive event handling
RepositoryTrait (Port)Infrastructure abstraction

Anti-Patterns to Avoid

Primitive Obsession

rust
// BAD: String có thể chứa gì cũng được
fn create_user(email: String, name: String, age: i32) { ... }

// GOOD: Types encode meaning
fn create_user(email: Email, name: NonEmptyString, age: PositiveInt) { ... }

Leaky Abstraction

rust
// BAD: Domain biết về database
pub struct User {
    pub id: i64,  // Database auto-increment leaking
}

// GOOD: Domain-specific ID
pub struct User {
    pub id: UserId,  // Opaque, có thể là UUID, ULID, etc.
}