From ddd
Eric Evans' supple design patterns (Domain-Driven Design, Ch. 10). Use when reviewing code for domain-model clarity, designing value objects, assessing whether operations compose cleanly, or reasoning about intention-revealing interfaces, side-effect-free functions, assertions, conceptual contours, standalone classes, closure of operations, declarative design, and specifications. Provides per-pattern intent, concrete detection signals for code review, remediation sketches, and pattern interactions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ddd:supple-designThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A reference for Eric Evans' supple design patterns as described in *Domain-Driven Design* (Ch. 10). Supple design is the style that makes a deep domain model usable — code that reads like the ubiquitous language, composes cleanly, and makes changes safe.
A reference for Eric Evans' supple design patterns as described in Domain-Driven Design (Ch. 10). Supple design is the style that makes a deep domain model usable — code that reads like the ubiquitous language, composes cleanly, and makes changes safe.
Supple design is not a universal requirement. It has a real cost in up-front modeling effort. Evans is explicit: invest in supple design where deep model insight pays off — the Core Domain, and the hot spots that domain experts keep returning to. Generic CRUD, admin tooling, configuration objects, and thin infrastructure wrappers are usually fine without it.
This document defines the eight patterns, each with the same structure:
Ruby/Rails idioms appear inline as illustrative examples. The patterns themselves are language-agnostic; the Ruby notes sharpen detection in the ecosystem this reference is most often used against.
Intent. Name classes, methods, and arguments so a reader can use them with confidence without opening the implementation. The name should reveal the effect and the purpose — the what and the why — not the how. When a method's name captures the business concept exactly, it disappears into the ubiquitous language.
Signals.
process, handle, manage, execute, do*, perform, run.*Util, *Helper, *Manager, *Processor, or an overly broad *Service attached to a domain object.calculate, compute, update, set*, insertOrUpdate, doTransaction.getTotal that also persists, a validate that also sends email).arg, data, obj, params, options on domain APIs.Not a violation when. The class is infrastructure (a serializer, a connection pool, a generic collection) or a technical utility. Generic names are correct when the concept is generic.
Remediation sketch. Rename toward the domain. OrderManager.process(order) becomes Order#place, Order#fulfill, Order#cancel — each a distinct concept in the language. Split methods whose behavior exceeds their name.
Interactions. Intention-Revealing Interfaces are a prerequisite for Declarative Design — declarative code reads as a composition of intentions, which only works if each name is a faithful intention. They also depend on Side-Effect-Free Functions, because a name that hides a side effect is not actually revealing.
Intent. Separate commands (operations that change state) from queries (operations that compute and return a value without observable side effects), and push as much logic as possible into the query side. Side-effect-free functions can be composed, re-evaluated, tested, and reasoned about locally. Commands remain small, explicit, and well-marked.
Signals.
order.addItem(i) that returns the new total; cart.apply(coupon) that mutates cart and returns the discount.void setters invoked by an application service that assembles the result.Not a violation when. The side effect is the point — a well-named command (Order#place, Account#withdraw) can return a result (a Receipt, a Transaction) as long as the name makes the side effect explicit. The CQS rule is strong, but a returning command named for the command is not hidden.
Remediation sketch. Lift computation into a pure function on an immutable value object. Have commands delegate to a pure function to compute the new state, then perform the single, well-named mutation. Favor returning new instances over mutating.
Interactions. Enables Closure of Operations (pure ops compose; mutating ops do not). Enables Assertions (preconditions/postconditions are clean on functions). Required for Declarative Design.
Intent. State the post-conditions of operations and the invariants of aggregates explicitly, so behavior is characterized independently of implementation. Assertions make the model's rules visible at the point where they must hold.
Signals.
Not a violation when. The type system already encodes the invariant (a non-null typed parameter in a strict language; an enum with exhaustive cases). Trust the type.
Remediation sketch. Move invariants onto the domain object itself — constructor validation, guarded setters (or better, no setters), explicit ensure/assert/guarded methods. Replace primitive obsession with value objects whose constructors enforce validity; an instance's existence is then proof of its validity. In Ruby, Email = Data.define(:value) do ... end with a validating constructor. Promoting each key of a hash-shaped concept to a named attribute is itself the act of stating the contract — the hash has no preconditions, the value object does.
Interactions. Assertions are how you know Side-Effect-Free Functions and Intention-Revealing Interfaces are honest. They give Specification its teeth.
Intent. Decompose along the natural fault lines of the domain, so cohesive concepts stay together and changes ripple through as few places as possible. The structure of the code should match the structure of the concepts, at whatever granularity is stable.
Signals.
UserService that validates emails, computes tax residency, issues refunds, and sends notifications).*Controller, *Service, *Helper) holding logic that belongs together on a domain object.Not a violation when. The split is the conceptual contour — e.g., Order vs. OrderLine is a real domain distinction, even though they change together. The test is: does a domain expert recognize the split as meaningful, or does it feel like a plumbing artifact?
Remediation sketch. Merge or reshape until a single domain change can be made in a single place. Where a concept is distributed, name and extract it. Let the ubiquitous language drive the seams.
Interactions. Good contours make Standalone Classes possible (fewer cross-contour dependencies) and make Intention-Revealing Interfaces easier to name (the concept is already distilled).
Intent. Reduce a class's dependencies to the bare minimum so it can be understood in isolation. A standalone class makes no reference to other domain types beyond what is essential to its concept. Low implicit coupling means low cognitive load.
Signals.
obj["type"], dict["labels"], h[:name]) where the shape is a domain concept waiting to be named — a missing value object. Three or more call sites indexing by the same string keys is a strong signal.Not a violation when. Aggregates need roots to reference their members; entities reference value objects. These are essential domain connections, not coupling for convenience.
Remediation sketch. Lift behaviors onto the concept they most belong to. Replace dependencies with primitives or value objects where possible — Data.define in Ruby (≥ 3.2), @dataclass(frozen=True) in Python, data class in Kotlin, case class in Scala are the idiomatic immutable value-object shapes. Pass in only the data the operation needs, not the whole graph. Push infrastructure out of the model layer.
When two concrete value objects share most of their behavior, the first reach is a module/mixin with two implementations, not an abstract base class with subclasses. Inheritance is a deliberate later choice when the hierarchy itself carries domain meaning (a true Liskov-substitutable relationship). In Ruby, that looks like:
module Field
def mirror_expectations = raise NotImplementedError
end
ScoreField = Data.define(:name, :scale) do
include Field
def mirror_expectations = { scale: scale }
end
TextField = Data.define(:name, :labels) do
include Field
def mirror_expectations = { labels: labels }
end
The module states the contract; each Data.define class promotes previously implicit hash keys (scale, labels) to explicit attributes.
Interactions. Standalone Classes are what Conceptual Contours produce when the contours are clean. They are reinforced by Closure of Operations (operations closed over a type don't import others).
Intent. Where it fits the domain, define operations whose argument and return types are the same — an operation closed over a type. Closure yields an algebra: operations compose without leaving the type, which is how you get fluent, expressive domain code.
Signals.
Self. Money.add(Money) returning BigDecimal. DateRange.extend(Duration) returning a tuple. Path.append(String) returning String.T.op(T) -> T — e.g. merging filters, unioning sets, combining price adjustments.Not a violation when. The operation genuinely crosses types (a Customer ordering a Product yields an Order — three different concepts). Don't force closure.
Remediation sketch. Return Self from value-object operations. If an operation's arguments include a foreign type, look for a specialized closed operation it can be expressed in terms of (e.g., Money.scale(factor) closed over Money, rather than Money * BigDecimal -> Money). In Ruby, Money = Data.define(:amount, :currency) do def +(other); ...; end end is the natural shape — Data.define gives structural equality and immutability for free, so closed operations compose without the usual ceremony.
Interactions. Closure depends on Side-Effect-Free Functions (you cannot compose mutating operations without tears) and is what Standalone Classes feel like in the value-object layer.
Intent. Describe what is true in the domain, rather than the procedure for computing it. Declarative domain code often takes the form of a small internal DSL, a combinator library, or a Specification-based query/validation layer. The reader sees intent; the machinery sits underneath.
Signals.
Not a violation when. The logic is genuinely sequential and stateful (a checkout workflow with external side effects at each step; a saga). Forcing declarativity on inherently procedural behavior obscures rather than reveals.
Remediation sketch. Introduce a Specification (see §8) or small combinator API to name the predicate. Replace duplicated checks with references to the spec. Where the rule is complex, a small internal DSL — even a few chainable factory methods — often reads like the ubiquitous language.
Interactions. Declarative Design is the payoff of Intention-Revealing Interfaces, Side-Effect-Free Functions, and Closure of Operations put together. Specification is its most common incarnation.
Intent. Encapsulate a predicate — a question you can ask of a domain object — as a first-class object. Specifications can be composed (and, or, not), reused for validation, selection, and construction, and moved between layers without losing meaning.
Signals.
order.total > 100 && !order.customer.vip && order.items.any { it.digital } in three services.findOrders(minTotal, excludeVip, digitalOnly, ...)) — the predicate wants to be an object.Not a violation when. The predicate is one-off, narrow, and unlikely to be reused. Do not introduce a Specification for a single caller — introduce it when the duplication appears or when the rule has a name in the ubiquitous language.
Remediation sketch. Extract the predicate as XSpecification with an isSatisfiedBy(candidate) method. Compose via and, or, not. Use the same spec for validation (assert on entry), selection (translate to a query predicate in the repository), and construction (guide factories). Name the spec after the business concept.
Interactions. Specification is the classic vehicle for Declarative Design in DDD. It depends on Intention-Revealing Interfaces (the spec name is a concept in the language) and Side-Effect-Free Functions (a predicate with side effects is a category error).
When reviewing code with this reference loaded, keep these cross-cutting rules in mind:
Data.define, not class Foo; attr_reader; def initialize…. Python: @dataclass(frozen=True). Kotlin: data class. Scala: case class. When two concrete types share most behavior, reach first for a module/mixin with two implementations; reach for inheritance only when the hierarchy itself carries domain meaning.npx claudepluginhub rpazevedo/ddd-plugin --plugin dddCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.