agentsclimarketplace

Ddd tactical

Skill kinhluan/skills/.agent-skills/ddd-tactical

πŸš€ Professional Multi-Agent Skills

Install
npx -y skills add kinhluan/skills --skill ddd-tactical

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Tactical Domain-Driven Design (DDD) with Scoring Rubric. Use this skill when designing internal domain models or performing architectural reviews to ensure domain logic is isolated and rich.

SKILL.md

16.9 KB, as published. Nobody here has run it

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.id means a == 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 string for email, int for 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 < 0:
            raise ValueError("Amount cannot be negative")
        if self.currency not in {"USD", "EUR", "VND", "JPY"}:
            raise ValueError(f"Invalid currency: {self.currency}")

    def add(self, other: "Money") -> "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:

CriterionPointsCheck
Immutability2Value Objects are immutable, created via factory/constructor with validation
Invariants2Aggregate Roots enforce business rules before any state change
Encapsulation2Internal aggregate parts hidden; only AR exposes public methods
Identity2Entities have stable, globally unique IDs; VOs have value equality
Ubiquitous Language2Class/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-PatternScoreSymptomFix
Anemic Model0/10Entities with only getters/setters, logic in ServicesMove business rules into entities
God Aggregate3/10Single aggregate with 10+ entitiesSplit by consistency boundary
Primitive Obsession5/10string email, int age instead of VOsCreate Email, Age VOs with validation
Leaking Domain4/10Returning domain entities in API responsesUse DTOs at boundary
Missing Invariants3/10Invalid states possible (negative quantity)Validate in constructor/methods
Getter/Setter Abuse2/10Public setters allow any state changeMake fields private, expose behavior methods

πŸ“¦ Tactical Artifacts

After designing the tactical model, create:

docs/domain/
β”œβ”€β”€ model-summary.md          # All aggregates, entities, VOs
β”œβ”€β”€ logic-rules.md            # Business invariants enforced by ARs
β”œβ”€β”€ ubiquitous-language.md    # Glossary: business term β†’ code term
└── diagrams/
    └── class-diagram.mermaid # UML of key aggregates

πŸ“š References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.