with one click
ddd
Domain-Driven Design architecture patterns and conventions for this project
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Domain-Driven Design architecture patterns and conventions for this project
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Initialize Claude Code in a repository with standard project scaffolding. Use this skill whenever the user wants to set up Claude Code in a new repo, initialize claude configuration, run /init with extras, or mentions "ray-init". This goes beyond the built-in /init by adding gitignore rules, Sideways files, and a local planning doc.
Test-Driven Development discipline for writing code. Use this skill whenever writing new features, adding behavior, refactoring with tests, fixing bugs with regression tests, or when the user mentions TDD, red-green-refactor, test-first, failing tests, or asks to write tests before implementation. Also triggers when branch plans contain FAILING test tasks or test-first instructions.
Create a plan document for the current branch, or for a specified new/existing branch. The complete template and all instructions are provided below — do not search for examples elsewhere.
Plan and track a bugfix with diagnosis-first discipline — create a BUGFIX.<branch>.md working doc, trace the reported symptom to a proven root cause, and write a failing test that reproduces the bug BEFORE fixing it. Use when the user reports a bug to fix, asks to investigate or diagnose a defect, starts a bugfix branch, or mentions "ray-bugfix-plan". The complete template and all instructions are below — do not search for examples elsewhere.
Audit skill source files for personal references, credentials, and other publication blockers before mirroring them into the ray-ai-skills public repo. Use this skill whenever the user wants to audit one or more skills, check skills before publishing/syncing, look for hardcoded paths or sensitive content in skills, or asks "audit my skills". If no specific skills are named, audit every skill that has changed since the last sync (or is new and not yet mirrored).
Update the ray-ai-skills repo from source skills under ~/.claude/skills/ — audit, then sync, then stop before commit. Also handles adding a new skill to .shared-skills, removing a skill from the mirror, or auditing without syncing. Use this skill whenever the user wants to sync skills into the repo, publish skill changes, update the skills mirror, add a skill to the shared list, remove a skill from the shared list, or says "skills-update". Accepts natural-language targets like "sync all", "update ray-tdd and ray-commit", "remove ray-foo", "audit only", etc.
| name | ddd |
| description | Domain-Driven Design architecture patterns and conventions for this project |
Domain-Driven Design architecture patterns and conventions.
Look at relevant portions of the current codebase's DDD if needed, or else request a reference project if unsure the current project is a good fit.
See CLAUDE.md → "Architecture" for layer paths, file conventions, and key examples.
domain/ # Pure domain (no framework dependencies)
├── types # Shared constrained types
├── <context>/
│ ├── entities/ # Aggregate roots and entities
│ ├── values/ # Value objects
│ └── policies/ # Domain policies (business rules, actor-agnostic)
infrastructure/
├── database/
│ ├── orm/ # ORM models (thin, no business logic)
│ └── repositories/ # Maps ORM ↔ domain entities
├── <external>/ # External API adapters (Gateway + Mapper)
application/
├── services/ # Use cases, orchestration
├── policies/ # Application policies (actor-dependent authorization)
├── responses/ # Response DTOs
presentation/
└── representers/ # Serialization for API responses
Dependencies flow inward only. Domain is at the center, knows nothing about outer layers.
Allowed:
repositories/ → imports domain/entities/services/ → imports domain/, repositories/, policies/controllers/ → imports services/Forbidden:
domain/ → NEVER imports from infrastructure, application, or presentationThree distinct concepts, often conflated:
Domain logic = intrinsic computations, always true regardless of context. "These two points are 32km apart." Pure math — belongs in value objects and entities.
Domain policies = business rules a domain expert would articulate, actor-agnostic. "Attendance must be within 55m of the event location." The threshold is a business decision (not a deployment decision), but the rule itself doesn't reference who is acting. Constants for thresholds belong in the domain, not in config files or infrastructure.
Application policies = rules that depend on who is acting or application-level context. "Only teaching staff can view all attendance records." These reference roles, requestors, or use-case context.
The key constraint: The domain layer can't know about application concepts like "who is the requestor" or "what role do they have."
Heuristic: If the rule is actor-agnostic (a domain expert would state it without mentioning roles) → domain/. If it references roles, requestors, or use-case context → application/policies/.
| Concern | Layer | Why |
|---|---|---|
| Distance calculation (Haversine) | Domain (value object) | Pure math, always true |
| "Right place, right time" | Domain (policy) | Business rule, actor-agnostic |
| "Only students must comply" | Application (service orchestration) | Depends on actor role |
| "Only staff can view all records" | Application (policy) | Depends on actor role |
Group related domain rules into a single policy when they answer the same domain question (e.g., proximity + time window = "is this attendance eligible?").
Anti-pattern: policy decisions in services. Services must NOT contain business rule logic — even simple conditionals like threshold comparisons. If a domain expert would articulate the rule, it belongs in a policy, not as an if statement in a service. Services call policies; they don't replicate them.
Evolution: If a threshold might vary (per course, per campus), make it a value object rather than a constant. The threshold evolves from a constant to a repository-backed lookup without architectural refactoring.
Two building blocks that are often misclassified:
Entities have identity — they are distinct things the domain recognizes, references, and acts upon. Two entities with identical attributes but different identities are different things.
Value Objects are attributes of entities — they describe aspects of entities with no identity of their own. Two value objects with the same attributes are interchangeable.
Value objects belong to entities. Every value object should have a parent entity it describes:
| Value Object | Describes Entity |
|---|---|
TimeRange | Course, Event |
GeoLocation | Location, Attendance |
CourseRoles | Enrollment |
If a domain concept doesn't naturally belong as an attribute of any existing entity, it is likely an entity itself. A standalone concept that someone reads, references, or acts upon — that's an entity, not a value object.
Reports, invoices, statements, and similar derived/computed concepts are entities when:
An invoice regenerated from the same order has the same attributes, yet invoices are canonical DDD entities. The same reasoning applies to reports.
Identity is a domain concept, not a storage decision. A computed report that is never persisted can still be a domain entity if the domain expert treats it as a distinct thing. Persistence is an infrastructure concern.
Delivery CaveatEvans' DDD Sample models Delivery as a value object, but Delivery belongs to Cargo (its parent entity). This pattern applies when a computed concept is an attribute of an aggregate. When a computed concept has no parent entity and stands alone, the entity classification is more appropriate.
Entities and value objects take their collaborators in the constructor and compute derived data on demand via memoized methods. Plain Ruby classes, not Dry::Struct.
Principle: Objects own their computation. The constructor receives domain objects; derived values are exposed as methods. No procedural factory that pre-computes everything and stuffs it into a passive struct.
Entity example — takes dependencies, computes on demand:
class AttendanceReport
ReportEvent = Data.define(:id, :name) # simple immutable data: use Data.define
attr_reader :course_name, :generated_at
def initialize(course:, attendances:)
@course_name = course.name
@generated_at = Time.now
@course = course
@attendances = attendances
end
def events
@events ||= raw_events.map { |e| ReportEvent.new(id: e.id, name: e.name) }
end
def student_records
@student_records ||= students.map do |enrollment|
StudentAttendanceRecord.new(enrollment:, events: raw_events, lookup: index)
end
end
private
def raw_events = @course.events_loaded? ? @course.events : []
def students = @course.enrollments_loaded? ? @course.students : []
def register = @register ||= AttendanceRegister.new(attendances: @attendances)
end
Value object example — computes from collaborators, provides value equality:
class StudentAttendanceRecord
attr_reader :email
def initialize(enrollment:, events:, lookup:)
@email = enrollment.account_email
@account_id = enrollment.account_id
@events = events
@lookup = lookup
end
def event_attendance
@event_attendance ||= @events.each_with_object({}) do |event, hash|
hash[event.id] = @lookup.attended?(@account_id, event.id) ? 1 : 0
end
end
def attend_sum = @attend_sum ||= event_attendance.values.sum
def attend_percent = @attend_percent ||= # ...compute from attend_sum and events
def ==(other)
other.is_a?(self.class) && email == other.email && event_attendance == other.event_attendance
end
alias eql? ==
def hash = [email, event_attendance].hash
end
When to use what:
| Need | Use |
|---|---|
| Object with behavior / computed methods | Plain Ruby class |
| Simple immutable data holder (2–3 fields, no logic) | Data.define |
| Value equality | Implement ==, eql?, hash |
Anti-pattern: Dry::Struct + .build factory that procedurally computes values and stores them as inert attributes. This separates computation from the object that should own it, producing a "dumb struct" filled by an external procedure.
When an entity holds a collection of children (e.g., a Course has Events), wrap the collection in a typed value object rather than using a raw Types::Array:
class Events
attr_reader :items
def initialize(items)
@items = items.freeze
end
def find(id) = items.find { |e| e.id == id }
def count = items.size
def to_a = items.dup
# ...domain-specific queries
end
Naming: Use plural nouns (Events, Locations, Enrollments) — consistent with the existing SystemRoles and CourseRoles convention, and natural in domain language (course.events.find(id)).
Benefits: type safety (only Entity::Event members), encapsulated query logic (move find_event, event_count off the parent entity), and the parent entity stays focused on its own concerns.
Coercion for ergonomics: Use a type constructor that auto-wraps raw arrays into the collection object. This keeps test construction simple (events: [event1, event2]) while repositories use the explicit form (Events.new(events)).
Not all "not loaded" states need a Null Object. The decision depends on how the collection flows through the system:
| Pattern | When to use | Example |
|---|---|---|
| Null Object | The attribute is passed polymorphically across layers (policies, auth, services) and callers shouldn't need to check for presence | SystemRoles / NullSystemRoles — Account.roles flows through policies and auth adapters that call .admin?, .has?() etc. |
| Optional nil | The attribute is accessed only after deliberate loading; callers choose their loading method upfront and know what they have | Course child collections — services call find_with_events or find_id and know whether children are present |
Heuristic: If the object crosses module boundaries and receivers call methods on it without knowing whether it was loaded, use a Null Object. If access is local and the caller controls loading, nil is simpler — a NoMethodError on nil clearly signals "you forgot to load."
Services are use cases. Each service is a single operation with railway-oriented flow (each step succeeds or short-circuits on failure).
Key principles:
ok, created, bad_request, forbidden, etc.) wrap results with HTTP-friendly statusTypical step flow:
Keep validation in services. Avoid premature abstraction.
Why validation belongs in services:
Controller responsibility is minimal: parse input, call service, pattern match on result.
When to extract validation:
Services often compose data from multiple repositories — an event with its location coordinates and course name, or a course with enrollment roles. These composites aren't domain entities (nobody says "enriched event"). They're application-layer concerns: the shape of what the use case returns.
Response DTOs live in application/responses/ and use Data.define.
Implementation:
# app/application/responses/event_details.rb
module Tyto
module Response
EventDetails = Data.define(
:id, :course_id, :location_id, :name, :start_at, :end_at,
:longitude, :latitude, :course_name, :location_name
)
end
end
The service builds the DTO from its repository results:
def enrich(event, location, course)
Response::EventDetails.new(
id: event.id, course_id: event.course_id, location_id: event.location_id,
name: event.name, start_at: event.start_at, end_at: event.end_at,
longitude: location&.longitude, latitude: location&.latitude,
course_name: course.name, location_name: location&.name
)
end
The representer serializes it — with a guaranteed shape, no respond_to? guards needed.
When to use response DTOs vs. passing entities directly:
| Situation | Use |
|---|---|
| Response matches a single entity's shape | Pass the entity directly to the representer |
| Response combines data from multiple entities | Response DTO (Data.define) |
| Response adds computed/derived fields not on the entity | Response DTO |
Anti-pattern: OpenStruct for composing multi-entity responses. OpenStruct has no guaranteed shape — the representer must use respond_to? guards, and typos in field names silently produce nil instead of raising errors.
Variant DTOs for different endpoints: When two endpoints return nearly the same shape but one has extra fields (e.g., user_attendance_status on a requestor-aware endpoint), use separate DTOs rather than one DTO with nil fields. This makes each endpoint's contract explicit and avoids conditional serialization logic.
External API integrations use Gateway + Mapper:
Dependency direction: Service → Mapper → Gateway. The mapper depends on the gateway, not the other way around. The gateway has no knowledge of the mapper.
Services inject the Mapper, not the Gateway. This means:
mapper.upload(video))Request → Controller parses input
↓
Service.call()
↓
step validate_input
↓
step authorize (application policy)
↓
step check_domain_rules (domain policy)
↓
step persist/fetch
↓
Success(response) or Failure(response)
↓
Controller pattern matches result
↓
Representer serializes success data
↓
Response ← JSON/etc. with status from response DTO
Seminal DDD resources for deeper exploration.
Cargo aggregate with Delivery value object and Itinerary demonstrates computed domain concepts: dddsample-core — characterization