Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/ddd-engineer-skills.version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1194355 docs(skills): 📝 Add @haskou/flow skill
427 changes: 427 additions & 0 deletions .agents/skills/ddd-engineer/SKILL.md

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions .agents/skills/ddd-engineer/references/aggregates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Aggregates and aggregate roots

Use this reference when the work touches aggregate design, entity ownership, repository boundaries, lifecycle transitions, invariants, or domain events.

## Aggregate purpose

An aggregate is a consistency boundary. It groups the aggregate root, owned entities, and value objects that must change together to preserve invariants.

Do not introduce an aggregate only to organize files. Introduce or modify an aggregate when there is a real lifecycle, ownership boundary, invariant, or transactional consistency rule.

## Aggregate roots

The aggregate root is the only object outside the aggregate that should be loaded, saved, or referenced directly by repositories and application use cases.

External code should not mutate child entities directly. It should call intent-revealing behavior on the root, such as `conversation.addParticipant(identity)`, `community.archiveChannel(channelId, byIdentity)`, or `order.confirm(payment)`.

Use cases coordinate aggregate roots; they should not reach through the root to enforce child-entity rules.

Aggregate roots should not expose public assertion methods. A public `assertCan...`, `assertIs...`, or `assertHas...` method makes callers orchestrate preconditions instead of asking the aggregate to perform behavior or answer a domain question.

Assertion helpers on an aggregate root should be private implementation details. If there are too many of them, extract a cohesive validation class, policy, or domain service with a clear concept name, such as `UserValidator.assertIsAdmin(userId)`. If callers need the boolean form too, keep `isAdmin(userId)` with that same validator/policy instead of duplicating the rule elsewhere.

Repositories should normally be organized around aggregate roots, not every entity. Avoid repositories for child entities unless the existing codebase has a deliberate exception.

## Invariants and transactions

Keep invariants that must be immediately consistent inside a single aggregate boundary.

If a rule needs data owned by another aggregate or bounded context, coordinate through application services, domain events, policies, read models, jobs, or process managers instead of making cross-aggregate mutations from inside the domain object.

Do not inject repositories, external clients, event buses, clocks, or framework services into aggregate roots unless the local architecture explicitly permits it. Prefer passing already-obtained value objects, domain services, policies, or timestamps into behavior.

## References between aggregates

Prefer references by identity value object rather than embedding another aggregate.

A root may receive another aggregate as a method argument when the domain language and local pattern justify it, but it should not take ownership of that aggregate’s lifecycle.

Avoid primitive ID comparison outside domain behavior. Add methods such as `belongsTo(communityId)`, `isOwnedBy(identityId)`, or `canBeManagedBy(identity)` when the question is domain-specific.

## Child entities and value objects

Use child entities when identity within the aggregate matters over time.

Use value objects when equality is structural and the object has no independent lifecycle.

Do not promote a child entity to an aggregate root only because a controller or repository wants direct access. First check whether the application flow should load the owning aggregate root and call behavior on it.

## Domain events from aggregates

Domain events emitted by aggregates should describe facts that happened in ubiquitous language, such as `CommunityChannelArchived` or `MessageSent`.

Events should be raised as a result of aggregate behavior, not assembled externally as transport payloads with domain-like names.

Keep integration payload mapping at the boundary. The domain event may later be mapped to pub/sub, websocket, email, or job payloads.

## Smells

- A repository per database table rather than per aggregate root.
- Use cases modifying child entities directly.
- Controllers deciding lifecycle transitions.
- Domain events named after transport actions rather than facts.
- Aggregates that expose setters for every field.
- Aggregate roots exposing public assertion methods.
- Large clusters of private assertions that should be a named validator, policy, or domain service.
- Aggregate roots that need many unrelated services to enforce behavior.
- A large aggregate used to avoid eventual consistency where a process or event would be clearer.
- Empty aggregate folders created before any aggregate code exists.
50 changes: 50 additions & 0 deletions .agents/skills/ddd-engineer/references/bounded-contexts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Bounded contexts and context maps

Use this reference when a change crosses modules, teams, subdomains, external systems, or language boundaries.

## Bounded context ownership

Treat each bounded context as owning its own model, language, persistence, and contracts.

Do not reuse entities, value objects, DTOs, enums, repositories, or domain events across contexts unless the project has an explicit shared-kernel convention.

If two areas use the same word with different meaning, keep separate model types and name the translation explicitly.

Inspect existing context/module boundaries before moving code. A name collision across contexts is a signal to clarify language, not to force reuse.

## Ubiquitous language discovery

Before naming new concepts, inspect existing domain terms in code, tests, feature files, API docs, event names, architecture docs, and business workflow descriptions.

Prefer terms already used by the business workflow over generic technical names.

When the user or codebase uses a domain term, preserve that language unless there is evidence it is inconsistent or outdated.

## Context relationships

When integrating with another context, identify the relationship before choosing a design:

- Shared kernel: a small, stable model intentionally owned by multiple contexts.
- Customer/supplier: downstream needs influence upstream behavior or contract.
- Conformist: downstream deliberately conforms to upstream’s model.
- Anticorruption layer: downstream protects its model from an upstream or legacy model.
- Open-host service: upstream exposes a stable integration surface.
- Published language: contexts exchange a documented public language through APIs, events, schemas, or contracts.

Do not introduce a formal context-map pattern unless it clarifies the change. Use the terms to choose boundaries and explain integration choices.

## Anticorruption layers

Use an anticorruption layer when external systems, legacy modules, or other bounded contexts use concepts that do not match this context’s domain model.

Translate external DTOs, statuses, enums, identifiers, and lifecycle meanings at the boundary. Do not let foreign models leak into the domain.

Name translation classes after the integration or published language, not after generic mapping mechanics, unless the codebase already has a mapper convention.

## Shared kernel

Use a shared kernel only for small, stable concepts that multiple contexts intentionally own together.

Shared-kernel changes should be treated as public-contract changes for all consuming contexts.

Do not create a shared kernel because duplication feels uncomfortable. Duplicated names across contexts may represent different concepts.
50 changes: 50 additions & 0 deletions .agents/skills/ddd-engineer/references/contract-changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Public contracts and integration boundaries

Use this reference when a change affects APIs, events, pub/sub messages, websockets, jobs, sync payloads, push notifications, persistence schemas, or downstream consumers.

## Boundary types

Treat these as explicit integration boundaries:

- API request and response DTOs.
- OpenAPI, GraphQL, protobuf, JSON schema, or other machine-readable contracts.
- Pub/sub payloads.
- Domain-event publication payloads.
- Websocket payloads.
- Job and scheduler payloads.
- Push notification payloads.
- External SDK payloads.
- Persistence models and migrations.

These shapes must not become domain models.

## Required updates when contracts change

When a public contract changes, check for and update:

- API docs.
- Machine-readable contract files.
- Resource/mapper/presenter/serializer tests.
- Acceptance or integration tests.
- Consumer-facing notes in the final response or PR description.

When an event, pub/sub, websocket, sync, push, or job contract changes, document:

- Payload shape.
- Routing topic/channel/event name.
- Recipients or consumers.
- Side effects.
- Expected consumer action.
- Compatibility or migration notes.

## Contract discipline

Routes/controllers should be thin: parse request, build a boundary message, call a use case, return a resource.

Resource, mapper, presenter, serializer, and projector classes are responsible for boundary shape. They should not make domain decisions.

Repositories hydrate and serialize aggregates. Domain code should not depend on persistence models.

Domain events should describe something that happened in ubiquitous language. They should not be transport DTOs with domain-ish names.

If a public contract changes, explain the change for downstream consumers in the final handoff.
33 changes: 33 additions & 0 deletions .agents/skills/ddd-engineer/references/cqrs-read-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# CQRS, queries, and read models

Use this reference when implementing queries, dashboards, search, reports, lists, counts, projections, or read-only screens.

## Command model versus query model

Use aggregate behavior for command-side decisions and lifecycle changes.

Do not hydrate aggregates just to serve read-only screens, lists, counts, dashboards, or search results if the project has read models, projections, query repositories, resources, or API views for that purpose.

Use read models and projections for query-side representation. They can be shaped for the caller, but they should not make domain decisions.

## Queries

A query object may receive primitives at the boundary and convert or validate them according to the local application pattern.

Query handlers or query use cases should express intent in ubiquitous language, not database mechanics.

Do not put lifecycle changes, invariant enforcement, or business decisions in query code.

## Projections

Projection code translates events or persisted state into query-optimized views.

Projection payloads and read-model storage are boundary concerns. Keep them out of the write-side domain model.

When changing a projection, consider backfill, rebuild, migration, idempotency, ordering, and compatibility with existing consumers.

## Acceptance tests

For read-side behavior, test the public query result or projection behavior that consumers depend on.

When a command changes a read model through events or projection updates, add integration or acceptance coverage if the project normally verifies that workflow end to end.
53 changes: 53 additions & 0 deletions .agents/skills/ddd-engineer/references/domain-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Domain events, integration events, and eventual consistency

Use this reference when raising, recording, publishing, consuming, or mapping events.

## Event types

Distinguish domain events from integration events.

Domain events are facts raised by domain behavior in ubiquitous language. They describe something that happened inside the model.

Integration events are published boundary contracts for other systems or bounded contexts.

Outbox records, broker messages, queue payloads, and websocket payloads are persistence or transport mechanics.

Map domain events to integration events at an application or infrastructure boundary according to local patterns.

## Domain event design

Name events in past tense using ubiquitous language, such as `MessageDeleted`, `MembershipAccepted`, or `ChannelArchived`.

Domain events should carry the information needed to understand the fact that happened. Do not turn them into transport DTOs with domain-ish names.

Do not put external routing, broker topics, websocket recipients, retry metadata, or transport headers into domain events unless the local architecture intentionally treats those as domain concepts.

## Event lifecycle

When an aggregate raises events, follow the project pattern for recording, pulling, clearing, persisting, publishing, or dispatching them.

Use application or unit-of-work code to coordinate persistence and publication. Do not publish directly from domain objects.

When aggregate changes can conflict, respect expected-version, revision, optimistic-concurrency, or event-stream patterns.

## Eventual consistency

Use eventual consistency for workflows that span aggregate or bounded-context boundaries.

Use a process manager, saga, application handler, scheduled job, or event consumer when a business process reacts to events and coordinates multiple steps over time.

Do not enlarge an aggregate only to avoid asynchronous coordination.

## Contracts and consumers

When an event is consumed outside the current module/context, treat its payload as a public contract.

Document payload shape, routing/topic, recipients, side effects, consumer action, ordering/idempotency assumptions, and compatibility notes when the project maintains such docs.

Add or update integration/contract tests when event payloads, routing, or consumer-visible behavior changes.

## Event sourcing

If the project uses event sourcing, rebuild aggregate state from recorded events and persist new events through the existing event-store pattern.

Do not mix state-based repository shortcuts into an event-sourced aggregate unless the project already has that convention.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Domain modeling decisions

Use this reference when deciding where a concept or behavior belongs.

## Choosing a model type

Use a value object when equality is structural, identity does not matter, and the concept is defined by its values. Value objects should be immutable when the local language and framework make that practical.

Use an entity when identity and lifecycle matter inside an aggregate. A child entity should usually be loaded, changed, and persisted through its aggregate root.

Use an aggregate root when external code needs a consistency, lifecycle, and transaction boundary. Other objects inside the aggregate should not be mutated from outside the root unless the local codebase has an explicit pattern for it.

Use a domain service only for domain behavior that belongs to the ubiquitous language, requires multiple domain objects, or cannot naturally live on a single aggregate, entity, or value object. A domain service should not orchestrate repositories, DTO mapping, transactions, logging, queues, or framework concerns.

Use a policy for a named, reusable business decision such as `MessageDeletionPolicy` or `ChannelManagementPolicy`. Do not extract one-line checks unless naming the rule clarifies the model or reuse is real.

Use a specification only when the codebase already uses specification-style predicates or when the predicate is a meaningful domain concept that benefits from composition.

Use a factory when creation has real domain meaning, multiple valid construction paths, generated identities, default invariants, or coordinated child-entity creation. Do not create factories just to hide constructors or pass through primitives.

Use a read model or projection when the use case is query-oriented and should not hydrate aggregates.

Use a process manager, saga, or job when a business process reacts to events and coordinates multiple steps over time across aggregate or bounded-context boundaries.

## Invariants versus validation

Input validation checks shape, required fields, formats, and boundary constraints. It belongs at the boundary: request schemas, message constructors, DTO mappers, or application input validation according to local patterns.

Domain invariants protect business truth. They belong in aggregate roots, entities, value objects, domain services, policies, or specifications.

Do not rely on controllers, request schemas, database constraints, or UI validation to enforce domain invariants. Those can provide defense in depth, but they should not be the only place business truth is protected.

## Lifecycle and state

Model lifecycle transitions explicitly: create, activate, suspend, archive, delete, restore, expire, publish, cancel, complete.

Prefer named transition methods over direct status mutation. Invalid transitions should be rejected by domain behavior using the project’s error/result pattern.

Avoid state strings that are interpreted in many places. Put state transitions and state questions behind domain behavior, value objects, enum-style domain values, or policies.

## Time and clocks

Pass time into domain behavior as a value object, timestamp, clock result, or project-specific time object.

Use clock abstractions at the application boundary when the project provides them. Do not call system time directly inside domain objects unless the local architecture permits it.

Model periods, windows, expirations, deadlines, and schedules as domain concepts when behavior depends on them.

## Domain failures

Use the project’s existing error/result pattern for domain failures.

Domain errors should name business failures, such as `CannotArchiveLastOwnerChannel` or `MembershipAlreadyAccepted`, not technical failures.

Do not throw generic errors for domain-rule violations if the project has typed domain errors, result objects, or failure values.
Loading
Loading