Install
$ agentstack add skill-kinhluan-skills-ddd-tactical ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Domain-Driven Design (DDD): Tactical Building Blocks & Review
Tactical DDD focuses on the technical implementation of rich domain models within a single Bounded Context. While Strategic DDD answers "What to build and where?", Tactical DDD answers "How do we model this in code?"
> "The heart of DDD is the Domain Model itself." — Eric Evans
🧱 Building Blocks Reference
1. Entity
Definition: An object with a unique identity that persists over time. Two entities with the same ID are the same entity, even if their attributes differ.
Characteristics:
- Has a globally unique, stable identity (UUID, auto-increment, natural key)
- Mutable state (attributes can change)
- Identity equality:
a.id == b.idmeansa == b - Encapsulates business logic (not just data + getters/setters)
Code Examples:
Go:
type Order struct {
ID uuid.UUID
CustomerID uuid.UUID
Items []OrderItem
Status OrderStatus
CreatedAt time.Time
}
func (o *Order) Cancel(reason string) error {
if o.Status == OrderStatusShipped {
return fmt.Errorf("cannot cancel shipped order")
}
if o.Status == OrderStatusCancelled {
return fmt.Errorf("order already cancelled")
}
o.Status = OrderStatusCancelled
o.recordEvent(OrderCancelledEvent{
OrderID: o.ID,
Reason: reason,
At: time.Now(),
})
return nil
}
func (o Order) Equals(other Order) bool {
return o.ID == other.ID // identity equality
}
Python:
from dataclasses import dataclass, field
from uuid import UUID, uuid4
from datetime import datetime
@dataclass
class Order:
id: UUID = field(default_factory=uuid4)
customer_id: UUID = field(default_factory=uuid4)
items: list = field(default_factory=list)
status: str = "PENDING"
created_at: datetime = field(default_factory=datetime.utcnow)
_events: list = field(default_factory=list, repr=False)
def cancel(self, reason: str) -> None:
if self.status == "SHIPPED":
raise ValueError("Cannot cancel shipped order")
if self.status == "CANCELLED":
raise ValueError("Order already cancelled")
self.status = "CANCELLED"
self._events.append(OrderCancelledEvent(
order_id=self.id,
reason=reason,
at=datetime.utcnow()
))
TypeScript:
class Order {
private _events: DomainEvent[] = [];
constructor(
public readonly id: UUID,
public readonly customerId: UUID,
public items: OrderItem[],
public status: OrderStatus = OrderStatus.PENDING,
public readonly createdAt: Date = new Date()
) {}
cancel(reason: string): void {
if (this.status === OrderStatus.SHIPPED) {
throw new Error('Cannot cancel shipped order');
}
if (this.status === OrderStatus.CANCELLED) {
throw new Error('Order already cancelled');
}
this.status = OrderStatus.CANCELLED;
this._events.push(new OrderCancelledEvent(this.id, reason, new Date()));
}
pullEvents(): DomainEvent[] {
const events = [...this._events];
this._events = [];
return events;
}
}
2. Value Object
Definition: An immutable object defined entirely by its attributes. Two VOs with the same values are equal. No identity.
Characteristics:
- Immutable (created once, never modified)
- Value equality: all fields must match
- Self-validating (invalid state impossible)
- Replace primitives (don't use
stringfor email,intfor age)
Code Examples:
Go:
type Money struct {
Amount decimal.Decimal
Currency string // ISO 4217
}
func NewMoney(amount decimal.Decimal, currency string) (Money, error) {
if amount.IsNegative() {
return Money{}, errors.New("amount cannot be negative")
}
if !isValidCurrency(currency) {
return Money{}, fmt.Errorf("invalid currency: %s", currency)
}
return Money{Amount: amount, Currency: currency}, nil
}
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, errors.New("cannot add different currencies")
}
return NewMoney(m.Amount.Add(other.Amount), m.Currency)
}
func (m Money) Equals(other Money) bool {
return m.Amount.Equal(other.Amount) && m.Currency == other.Currency
}
// Usage: always valid
price, _ := NewMoney(decimal.NewFromFloat(99.99), "USD")
discount, _ := NewMoney(decimal.NewFromFloat(10.00), "USD")
final, _ := price.Subtract(discount) // $89.99 USD
Python:
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
def __post_init__(self):
if self.amount "Money":
if self.currency != other.currency:
raise ValueError("Cannot add different currencies")
return Money(self.amount + other.amount, self.currency)
def __eq__(self, other):
if not isinstance(other, Money):
return False
return self.amount == other.amount and self.currency == other.currency
# Usage
price = Money(Decimal("99.99"), "USD")
discount = Money(Decimal("10.00"), "USD")
final = price.add(discount) # Money(109.99, "USD")
TypeScript:
class Money {
constructor(
public readonly amount: Decimal,
public readonly currency: string
) {
if (amount.isNegative()) {
throw new Error('Amount cannot be negative');
}
if (!VALID_CURRENCIES.has(currency)) {
throw new Error(`Invalid currency: ${currency}`);
}
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('Cannot add different currencies');
}
return new Money(this.amount.add(other.amount), this.currency);
}
equals(other: Money): boolean {
return this.amount.equals(other.amount) && this.currency === other.currency;
}
}
3. Aggregate Root
Definition: A cluster of associated objects treated as a single unit for data changes. The Aggregate Root is the only entry point — external code cannot reference internal entities directly.
Characteristics:
- One Aggregate Root (AR) per aggregate
- AR controls all state changes within the aggregate
- Internal entities are accessed only through the AR
- Transaction boundary: one transaction per aggregate
- Enforces invariants (business rules that must always be true)
Code Example (Go):
type Order struct { // Aggregate Root
ID uuid.UUID
CustomerID uuid.UUID
items []OrderItem // internal: accessed only through Order
status OrderStatus
total Money
events []DomainEvent
}
type OrderItem struct { // Entity within aggregate
ProductID uuid.UUID
Quantity int
UnitPrice Money
}
// Invariant: total must equal sum of item subtotals
func (o *Order) AddItem(productID uuid.UUID, quantity int, price Money) error {
if o.status != OrderStatusPending {
return errors.New("cannot modify non-pending order")
}
if quantity <= 0 {
return errors.New("quantity must be positive")
}
item := OrderItem{
ProductID: productID,
Quantity: quantity,
UnitPrice: price,
}
o.items = append(o.items, item)
o.recalculateTotal() // invariant enforcement
return nil
}
func (o *Order) recalculateTotal() {
total := MustNewMoney(decimal.Zero, o.total.Currency)
for _, item := range o.items {
subtotal, _ := item.UnitPrice.Multiply(item.Quantity)
total, _ = total.Add(subtotal)
}
o.total = total
}
// Invariant: cannot pay wrong amount
func (o *Order) Pay(payment Money) error {
if !o.total.Equals(payment) {
return fmt.Errorf("payment %v does not match total %v", payment, o.total)
}
o.status = OrderStatusPaid
o.recordEvent(OrderPaidEvent{OrderID: o.ID})
return nil
}
4. Domain Service
Definition: Stateless business logic that doesn't belong to any Entity or Value Object. Used when logic involves multiple aggregates or doesn't fit a single entity.
When to use:
- Calculations spanning multiple aggregates
- Operations that don't have a natural "owner"
- External service coordination (with domain rules)
Code Example:
// PricingService calculates order totals with discounts
// This logic doesn't belong to Order (doesn't own discount rules)
// or Product (doesn't know about orders)
type PricingService struct {
discountRepo DiscountRepository
}
func (s *PricingService) CalculateTotal(
items []OrderItem,
customer Customer,
couponCode string,
) (Money, error) {
subtotal := calculateSubtotal(items)
// Apply customer tier discount
tierDiscount := s.customerTierDiscount(customer.Tier)
afterTier, _ := subtotal.Multiply(1 - tierDiscount)
// Apply coupon if valid
if couponCode != "" {
coupon, err := s.discountRepo.FindByCode(couponCode)
if err != nil {
return Money{}, err
}
if !coupon.IsValidFor(customer, items) {
return Money{}, errors.New("coupon not applicable")
}
afterTier, _ = afterTier.Subtract(coupon.Amount)
}
// Add tax
tax := s.calculateTax(afterTier, customer.ShippingAddress.Country)
total, _ := afterTier.Add(tax)
return total, nil
}
5. Repository
Definition: An abstraction over persistence. The domain defines the interface; infrastructure provides the implementation.
Pattern:
Domain Layer → defines interface (port)
│
Infrastructure Layer → implements interface (adapter)
Go Example:
// domain/order/repository.go — defined in domain layer
package order
type Repository interface {
FindByID(ctx context.Context, id uuid.UUID) (*Order, error)
FindByCustomer(ctx context.Context, customerID uuid.UUID) ([]*Order, error)
Save(ctx context.Context, order *Order) error
Delete(ctx context.Context, id uuid.UUID) error
}
// infrastructure/persistence/postgres_order_repo.go — implemented in infra layer
package persistence
type PostgresOrderRepository struct {
db *sql.DB
}
func (r *PostgresOrderRepository) FindByID(
ctx context.Context,
id uuid.UUID,
) (*order.Order, error) {
// SQL implementation
}
func (r *PostgresOrderRepository) Save(
ctx context.Context,
o *order.Order,
) error {
// SQL implementation with event extraction
events := o.PullEvents()
for _, event := range events {
// publish to event bus
}
return nil
}
6. Factory
Definition: Encapsulates complex object creation logic. Used when creating an aggregate involves multiple steps or invariants.
Go Example:
type OrderFactory struct {
idGenerator IDGenerator
clock Clock
}
func (f *OrderFactory) CreateOrder(
customerID uuid.UUID,
items []OrderItem,
) (*Order, error) {
if len(items) == 0 {
return nil, errors.New("order must have at least one item")
}
order := &Order{
ID: f.idGenerator.Generate(),
CustomerID: customerID,
Items: items,
Status: OrderStatusPending,
CreatedAt: f.clock.Now(),
}
order.recalculateTotal()
order.recordEvent(OrderCreatedEvent{
OrderID: order.ID,
CustomerID: customerID,
Total: order.Total,
At: order.CreatedAt,
})
return order, nil
}
7. Domain Event
Definition: A record of something significant that happened in the domain. Events are immutable and represent past facts.
Pattern:
1. Event occurs inside aggregate
2. Aggregate collects events internally
3. Repository extracts events on save
4. Events are published to event bus
5. Other aggregates/consumers react
Go Example:
// Event definition
type OrderPaidEvent struct {
OrderID uuid.UUID
Amount Money
PaidAt time.Time
}
func (e OrderPaidEvent) EventName() string { return "order.paid" }
// Inside aggregate
func (o *Order) Pay(payment Money) error {
// ... validation ...
o.status = OrderStatusPaid
o.recordEvent(OrderPaidEvent{
OrderID: o.ID,
Amount: payment,
PaidAt: time.Now(),
})
return nil
}
func (o *Order) recordEvent(event DomainEvent) {
o.events = append(o.events, event)
}
func (o *Order) PullEvents() []DomainEvent {
events := o.events
o.events = nil
return events
}
8. Specification Pattern
Definition: Encapsulates query criteria as reusable, composable objects.
Go Example:
// Specification interface
type OrderSpecification interface {
IsSatisfiedBy(order Order) bool
ToSQL() (string, []interface{})
}
// Concrete specifications
type OverdueOrderSpecification struct {
Now time.Time
}
func (s OverdueOrderSpecification) IsSatisfiedBy(order Order) bool {
if order.Status != OrderStatusPaid {
return false
}
dueDate := order.CreatedAt.Add(7 * 24 * time.Hour)
return s.Now.After(dueDate) && order.Status != OrderStatusShipped
}
func (s OverdueOrderSpecification) ToSQL() (string, []interface{}) {
return "status = ? AND created_at < ?",
[]interface{}{OrderStatusPaid, s.Now.Add(-7 * 24 * time.Hour)}
}
// Composable specifications
type AndSpecification struct {
Left, Right OrderSpecification
}
func (s AndSpecification) IsSatisfiedBy(order Order) bool {
return s.Left.IsSatisfiedBy(order) && s.Right.IsSatisfiedBy(order)
}
// Usage
overdue := OverdueOrderSpecification{Now: time.Now()}
highValue := HighValueOrderSpecification{MinAmount: MustNewMoney(decimal.NewFromFloat(1000), "USD")}
critical := AndSpecification{Left: overdue, Right: highValue}
// In memory
for _, order := range orders {
if critical.IsSatisfiedBy(order) {
// handle critical overdue high-value order
}
}
// In database
sql, args := critical.ToSQL()
rows, _ := db.Query("SELECT * FROM orders WHERE "+sql, args...)
⭐ Domain Model Scoring Rubric (0-10)
Rate your domain design:
| Criterion | Points | Check | |-----------|--------|-------| | Immutability | 2 | Value Objects are immutable, created via factory/constructor with validation | | Invariants | 2 | Aggregate Roots enforce business rules before any state change | | Encapsulation | 2 | Internal aggregate parts hidden; only AR exposes public methods | | Identity | 2 | Entities have stable, globally unique IDs; VOs have value equality | | Ubiquitous Language | 2 | Class/method names match business language exactly |
Scoring:
- 10/10: Exemplary. Domain logic is fully encapsulated, rich, and self-validating.
- 8-9/10: Good. Minor gaps in encapsulation or language alignment.
- 6-7/10: Fair. Some anemic models, primitive obsession, or leaked logic.
- ≤5/10: Poor. Significant refactoring needed. Likely anemic domain model.
Goal: Target 8/10 or higher for Core Domains.
🚫 Tactical Anti-Patterns
| Anti-Pattern | Score | Symptom | Fix | |-------------|-------|---------|-----| | Anemic Model | 0/10 | Entities with only getters/setters, logic in Services | Move business rules into entities | | God Aggregate | 3/10 | Single aggregate with 10+ entities | Split by consistency boundary | | Primitive Obsession | 5/10 | string email, int age instead of VOs | Create Email, Age VOs with validation | | Leaking Domain | 4/10 | Returning domain entities in API responses | Use DTOs at boundary | | Missing Invariants | 3/10 | Invalid states possible (negative quantity) | Validate in constructor/methods | | Getter/Setter Abuse | 2/10 | Public setters allow any state change | Make fields private, expose behavior
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: kinhluan
- Source: kinhluan/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.