diff --git a/docs/README.md b/docs/README.md
index 6abba39..a3a77d0 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,6 +7,8 @@ This package provides DDD and Hexagonal Architecture building blocks for Python
- [Getting started](getting-started.md) — Step-by-step guide: install, define a domain model, handle commands, wire buses.
- [Component reference](component-reference.md) — Every abstract class, protocol, and infrastructure component.
- [Coding standards](coding-standards.md) — Conventions aligned with DDD and Clean Architecture, with do/don't guidelines.
+- [Architecture](architecture.md) — Service building blocks: API, database, subscriber, publisher, worker, outbox, observability.
+- [Best practices](best-practices.md) — Design rules for domain components, application layer contracts, and event/task selection.
## Complete working example
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..cf903e3
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,475 @@
+# Service Architecture — Building Blocks
+
+> **Key points reference.** This document describes the standard building blocks of a service built on top of the seedwork packages. It is intentionally concise. For deeper context, see the [references](#5-references) at the end.
+
+---
+
+## 1. Service Anatomy
+
+Not every service needs all seven blocks. Start with the API and the database. Add the remaining blocks only when the corresponding concern appears.
+
+```mermaid
+graph TB
+ Client([HTTP Client])
+ Broker([Message Broker])
+ Cron([Cron Trigger])
+
+ subgraph Service
+ direction TB
+
+ subgraph Entry Points
+ API[API]
+ SUB[Subscriber]
+ DLQS[DLQ Subscriber]
+ SCH[Scheduler]
+ end
+
+ subgraph Application Layer
+ Bus[CommandBus / QueryBus]
+ end
+
+ DB[(Database\n+ Outbox)]
+
+ subgraph Background Processes
+ PUB[Publisher]
+ WRK[Worker]
+ end
+ end
+
+ Client -->|HTTP request| API
+ Broker -->|integration events| SUB
+ Broker -->|failed messages| DLQS
+ Cron -->|trigger| SCH
+
+ API --> Bus
+ SUB --> Bus
+ DLQS --> Bus
+ SCH --> Bus
+
+ Bus <-->|read / write + outbox| DB
+
+ DB -->|integration event outbox| PUB
+ DB -->|task outbox| WRK
+
+ PUB -->|integration events| Broker
+```
+
+| Block | Responsibility | Required when |
+|---|---|---|
+| **API** | HTTP entry point | Always |
+| **Database** | Primary store + outbox tables | Always |
+| **Subscriber** | Consumes incoming integration events | Service reacts to external events |
+| **DLQ Subscriber** | Processes failed subscriber messages | Subscriber exists |
+| **Publisher** | Publishes outgoing integration events from outbox | Service emits integration events |
+| **Scheduler** | Triggers cron jobs on a time schedule | Service has periodic operations |
+| **Worker** | Processes background tasks from outbox | Service schedules background tasks |
+
+---
+
+## 2. The Building Blocks
+
+### 2.1 API
+
+The API is the **synchronous HTTP entry point**. Its only responsibility is to translate an HTTP request into a Command or Query, dispatch it through the bus, and map the result back to an HTTP response.
+
+#### Key points
+
+- Stateless. No business logic, no domain knowledge.
+- Commands and Queries are plain frozen dataclasses. Domain value validation is enforced in `__post_init__` of Value Objects and Entities — controllers do not validate.
+- Dispatches to `CommandBus` for writes (returns `Result`) and `QueryBus` for reads (returns `TResult | None`).
+- Maps `Result.failed` → 4xx. Maps `None` → 404. Infrastructure exceptions → 5xx via a global error handler.
+
+#### Do
+
+- Keep controllers thin: parse → dispatch → map response.
+- Let the bus stack handle validation, transactions, and domain event coordination.
+
+#### Don't
+
+- Put domain logic or repository calls in controllers.
+- Return domain entities directly — map to DTOs at the response layer.
+
+---
+
+### 2.2 Database
+
+The database is the **primary store and the outbox**. Both live in the same database, which is what makes the outbox guarantee possible.
+
+#### Key points
+
+- Aggregate tables and outbox tables are in the same database. The transaction that saves an aggregate also writes the outbox record — atomically.
+- Two outbox tables with independent lifecycles: `integration_event_outbox` and `task_outbox`.
+- The Unit of Work manages the transaction boundary. No transaction spans more than one command.
+- The database is the single source of truth for both business state and pending deliveries.
+
+#### Do
+
+- Keep the outbox in the same database as the aggregates.
+- Use separate outbox tables for integration events and background tasks.
+
+#### Don't
+
+- Write to the outbox outside of the command transaction.
+- Share a transaction across multiple commands or aggregate roots.
+
+---
+
+### 2.3 Subscriber
+
+The Subscriber **consumes incoming integration events from the message broker** and translates them into commands that the service can process.
+
+#### Key points
+
+- Maps each incoming integration event type to a Command and dispatches it through the `CommandBus`.
+- Acknowledges the message to the broker **only after** successful processing (commit + no exception).
+- On failure: does not acknowledge — the broker retries according to its retry policy.
+- Must deduplicate by event ID. The same event may arrive more than once (at-least-once delivery).
+- The subscriber is an infrastructure entry point — it is tightly coupled to the broker. It does not belong in the application or domain layers.
+
+#### Do
+
+- Dedup by event ID before dispatching the command (store processed IDs in the database).
+- Log the correlation ID and causation ID from the incoming event.
+
+#### Don't
+
+- Put domain logic in the subscriber.
+- Acknowledge before the command completes successfully.
+- Let one subscriber handle unrelated integration event types from different bounded contexts.
+
+---
+
+### 2.4 DLQ Subscriber
+
+The DLQ Subscriber **processes messages that exhausted all retry attempts** in the main subscriber.
+
+#### Key points
+
+- Reads from the Dead Letter Queue (or equivalent) of the broker.
+- Typical actions: log for human inspection, trigger an alert, attempt a corrective command, or discard after recording.
+- Do not blindly re-queue into the main queue — investigate the failure reason first. A business-rule failure (e.g. duplicate command, invalid state) will fail again regardless of retries.
+- Separates the concern of failed-message handling from the main processing loop.
+
+#### Do
+
+- Distinguish between transient failures (safe to re-queue) and deterministic failures (require human intervention).
+- Record the original event, failure reason, and attempt count for every DLQ message processed.
+
+#### Don't
+
+- Automatically re-queue all DLQ messages without inspection.
+- Mix DLQ handling with the main subscriber logic.
+
+---
+
+### 2.5 Publisher
+
+The Publisher **reads pending records from the integration event outbox and delivers them to the message broker**.
+
+#### Key points
+
+- Runs as an independent background process. It has no knowledge of the business operation that created the outbox records.
+- Reads records with status `pending`, publishes to the broker, then marks them `published`.
+- Never modifies business state — it only moves records through the outbox lifecycle.
+
+#### Delivery strategies
+
+| Strategy | Mechanism | Latency | Infrastructure overhead |
+|---|---|---|---|
+| **Polling** | `SELECT … FOR UPDATE SKIP LOCKED` at a fixed interval | Seconds | None — works with any DB |
+| **CDC** | Reads WAL/binlog via Debezium Server or equivalent | Milliseconds | Requires CDC tooling |
+| **Hybrid** | `pg_notify` / DB trigger as wake signal + polling as fallback | Near-real-time | Moderate |
+| **Native stream** | DB-native change feed / listener (see below) | Milliseconds | None — built into the DB |
+
+Default to polling. Only add CDC or a native stream when polling latency is a demonstrated bottleneck.
+
+> **Native change streams.** Several databases provide built-in mechanisms that eliminate the need for a separate polling loop or CDC tooling. When available, these are the lowest-overhead option:
+>
+> - **MongoDB** — Change Streams (`collection.watch()`): react to outbox inserts in real time, directly in application code.
+> - **Firestore / Firebase** — `onWrite` Cloud Function triggers: the platform calls your code on every document change.
+> - **Azure Cosmos DB** — Change Feed: a built-in, ordered stream of document changes consumable by a processor library.
+> - **DynamoDB** — DynamoDB Streams + Lambda: captures item-level changes and invokes a function per record.
+>
+> In all cases the outbox table/collection still exists — the native stream is only the delivery mechanism that replaces the polling loop. The atomicity guarantee (outbox written in the same transaction as the aggregate) remains unchanged.
+
+#### Retry and re-publication
+
+- On broker failure: retry with exponential backoff. Do not mark the record as `failed` on the first attempt.
+- After a configurable number of retries: mark as `failed` and alert. Do not discard.
+- Failed records must be re-publishable by an operator without reprocessing the original command.
+- Use `SKIP LOCKED` in polling queries to allow multiple Publisher instances without contention.
+
+#### Do
+
+- Run multiple Publisher instances for throughput — `SKIP LOCKED` handles concurrency safely.
+- Monitor the outbox for records that remain `pending` beyond a threshold — it signals a Publisher failure.
+
+#### Don't
+
+- Delete outbox records immediately after publishing — retain them for a configurable period for auditability.
+- Re-publish an already-published record. Use the `published` status check as a guard.
+
+---
+
+### 2.6 Scheduler
+
+The Scheduler **triggers periodic operations on a time-based schedule (cron)**. It translates a cron tick into a Command dispatched through the `CommandBus`.
+
+#### Key points
+
+- Each scheduled job maps to one Command. The handler contains all the logic for that job.
+- The Scheduler itself is stateless — it only fires the trigger. State lives in the aggregate and the database.
+- Jobs must be idempotent. In distributed environments, multiple instances of the Scheduler may fire the same job concurrently. Use a distributed lock or a database-backed job record to prevent duplicate execution.
+- Failures follow the same path as any command failure — `Result.failed` for domain errors, exceptions for infrastructure errors.
+
+#### Do
+
+- Design scheduled commands to be idempotent (safe to run twice).
+- Use a distributed lock or a database-backed lease to prevent concurrent execution of the same job.
+
+#### Don't
+
+- Put business logic in the Scheduler. It fires a command — the handler and the domain do the work.
+- Assume the Scheduler fires exactly once. It may fire zero times (missed schedule) or more than once (overlapping instances).
+
+---
+
+### 2.7 Worker
+
+The Worker **reads pending records from the task outbox and executes them by calling the appropriate `TaskHandler`**.
+
+#### Key points
+
+- Mirrors the Publisher's structure but for internal background tasks instead of external integration events.
+- Reads `TaskOutboxRecord` with status `pending`, calls the registered `TaskHandler`, marks as `delivered`.
+- `TaskHandler` implementations belong to the same service and codebase. No broker involved.
+- Handlers must be idempotent — at-least-once execution is the delivery guarantee.
+- On handler failure: retry with exponential backoff. After max retries: mark as `failed` and alert.
+
+#### Execution strategies
+
+##### Strategy A — In-process polling (default)
+
+The Worker polls the task outbox and calls the `TaskHandler` directly in the same process.
+
+```text
+TaskOutbox (pending) → Worker (SKIP LOCKED) → TaskHandler → mark delivered
+```
+
+No extra infrastructure. The simplest option and the right default for most services.
+
+##### Strategy B — Delegated to an external task queue
+
+A relay process reads the task outbox and *publishes* the task into the broker of an external task queue system (Redis, RabbitMQ, etc.). The task queue's own workers (Celery, BullMQ, Sidekiq, Laravel Horizon…) consume from that broker and invoke the `TaskHandler`.
+
+```text
+TaskOutbox (pending) → Relay (SKIP LOCKED) → Task queue broker → Queue worker → TaskHandler → mark delivered
+```
+
+The relay is structurally identical to the Publisher — it reads from the outbox and publishes to a destination. The destination here is the task queue broker instead of the integration event broker.
+
+The outbox record is marked `delivered` once the task queue broker **confirms the enqueue** — not when the `TaskHandler` finishes. From that point, the task queue system owns the task and is responsible for its own delivery guarantees, retries, and DLQ. This is the same contract as the Publisher with integration events: the outbox's responsibility ends when the destination confirms receipt.
+
+Use Strategy B when you need features the simple polling loop cannot provide: advanced scheduling, priority queues, rate limiting, concurrency controls, or worker autoscaling.
+
+> Do not bypass the outbox by publishing tasks directly into the queue from the command handler. The atomicity guarantee is lost if the service crashes after publishing but before committing the transaction.
+
+#### Do
+
+- Register one `TaskHandler` per task type.
+- Design handlers to be idempotent by task ID.
+- In Strategy B, treat the task queue as a delivery mechanism only — the outbox is the durable store.
+
+#### Don't
+
+- Use the Worker to communicate with other services — that is the Publisher's responsibility.
+- Perform fire-and-forget work without going through the outbox. Tasks not recorded in the outbox are lost on crash.
+- In Strategy B, wait for the `TaskHandler` to complete before marking `delivered` — the queue system owns execution from the moment of successful enqueue.
+
+---
+
+## 3. Infrastructure Mechanisms
+
+### 3.1 Outbox Pattern
+
+The outbox pattern **guarantees atomicity between a business state change and the intent to deliver an event or task**.
+
+```mermaid
+sequenceDiagram
+ participant Handler as CommandHandler
+ participant DB as Database
+ participant Publisher as Publisher / Worker
+ participant Dest as Broker / TaskHandler
+
+ Note over Handler,DB: Inside transaction
+ Handler->>DB: save aggregate
+ Handler->>DB: insert outbox record (status = pending)
+ Note over Handler,DB: Commit
+
+ Publisher->>DB: SELECT FOR UPDATE SKIP LOCKED (status = pending)
+ DB-->>Publisher: outbox record
+ Publisher->>Dest: deliver
+ Dest-->>Publisher: ack
+ Publisher->>DB: UPDATE status = published / delivered
+```
+
+#### Key points
+
+- The outbox record is written in the same transaction as the aggregate. Either both commit or neither does — no lost events.
+- The Publisher/Worker process is separate and runs independently of the command execution.
+- At-least-once delivery: if the Publisher crashes after delivering but before marking `published`, the record will be re-delivered on restart. Consumers must be idempotent.
+
+---
+
+### 3.2 Unit of Work
+
+The Unit of Work defines the **transaction boundary for a single command execution**.
+
+#### What is inside the transaction
+
+- `Repository.save` — aggregate state persisted.
+- `DomainEventBus.dispatch` — domain event handlers execute synchronously.
+- `IntegrationEventPublisher.publish` — outbox record written.
+- `TaskScheduler.schedule` — task outbox record written.
+
+#### What is outside the transaction
+
+- Actual broker delivery (Publisher process).
+- Background task execution (Worker process).
+- Read operations (`findById`, read repositories) — they participate in the transaction for consistency but do not require it for correctness.
+
+#### Do
+
+- Let `TransactionalCommandBus` manage the transaction transparently — handlers never call commit/rollback directly.
+
+#### Don't
+
+- Span a transaction across multiple commands.
+- Open a transaction in a query handler.
+
+---
+
+### 3.3 Idempotency
+
+**Every consumer of an at-least-once delivery channel must handle duplicates.**
+
+This applies to: Subscriber (incoming integration events), Worker (background tasks), and any downstream consumer of integration events your service publishes.
+
+#### Standard deduplication pattern
+
+1. Before processing: check if the event/task ID exists in a `processed_events` table.
+2. If found: acknowledge and return immediately (already processed).
+3. If not found: process + insert the ID into `processed_events` in the same transaction.
+
+#### Key points
+
+- Use the event or task `id` as the deduplication key — it is a stable UUID generated at event creation time.
+- The `processed_events` table can be pruned after a retention window (e.g. 30 days).
+- Idempotency is the consumer's responsibility. Do not assume the producer will deduplicate.
+
+#### Do
+
+- Implement deduplication at the subscriber and worker level.
+- Make `TaskHandler` implementations idempotent by design, even if the outbox already provides some protection.
+
+#### Don't
+
+- Rely on message broker deduplication guarantees — they vary by broker and configuration.
+- Use the event `occurred_at` timestamp as a deduplication key — timestamps are not unique.
+
+---
+
+### 3.4 Retry and DLQ
+
+**Not all failures are equal.** The retry strategy must distinguish between failure types.
+
+| Failure type | Origin | Action |
+|---|---|---|
+| Transient (network, timeout, DB unavailable) | Infrastructure | Retry with exponential backoff |
+| Deterministic (business rule violation, invalid state) | Domain | Do not retry — send to DLQ or discard |
+| Poison message (malformed, unparseable) | Data | Send to DLQ immediately — retrying will never succeed |
+
+#### Retry policy
+
+- Use exponential backoff with jitter to avoid thundering herd on recovery.
+- Set a maximum retry count per consumer. After exhaustion: route to DLQ.
+- Log each retry attempt with the failure reason, attempt number, and correlation ID.
+
+#### Dead Letter Queue
+
+- The DLQ is the last resort, not a recycle bin. Investigate before re-queuing.
+- Every DLQ message should trigger an alert.
+- Maintain a re-queue mechanism for messages that are safe to retry after a fix is deployed.
+
+#### Do
+
+- Configure retry limits and DLQ routing explicitly for each subscriber.
+- Treat DLQ messages as incidents that require investigation.
+
+#### Don't
+
+- Retry on `DomainError` — it is a deterministic failure.
+- Leave DLQ messages unmonitored.
+
+---
+
+## 4. Observability
+
+### 4.1 Motivation
+
+A single user action can trigger a chain of commands, domain events, integration events, and background tasks across multiple processes and time boundaries. Without explicit traceability fields, reconstructing that chain after a failure is guesswork.
+
+Two fields make the chain reconstructable:
+
+- **`correlation_id`** — the ID of the originating operation (typically the first HTTP request or incoming integration event). Constant throughout the entire chain. Answers: *what user action caused this?*
+- **`causation_id`** — the ID of the direct parent event, command, or task that caused this one. Changes at each step. Answers: *what immediately triggered this?*
+
+### 4.2 Propagation Rules
+
+```text
+HTTP Request (correlation_id = X, causation_id = X)
+ └── Command dispatched
+ └── DomainEvent emitted (correlation_id = X, causation_id = command.id)
+ └── IntegrationEvent published (correlation_id = X, causation_id = domainEvent.id)
+ └── BackgroundTask scheduled (correlation_id = X, causation_id = domainEvent.id)
+```
+
+#### Rules
+
+- `correlation_id` is **always** inherited from the parent — it never changes within a chain.
+- `causation_id` is the `id` of the immediate parent (the event, command, or task that triggered this one).
+- If there is no parent (e.g. a cron job or a DLQ re-queue), generate a new `correlation_id` and set `causation_id` to the trigger ID.
+
+### 4.3 Minimum Recommended Fields
+
+Every integration event, background task, and structured log entry should carry:
+
+| Field | Type | Description |
+|---|---|---|
+| `id` | UUID | Unique identifier of this event/task/log entry |
+| `correlation_id` | UUID | Originating operation identifier |
+| `causation_id` | UUID | Direct parent identifier |
+| `occurred_at` | ISO-8601 | When the fact occurred |
+| `type` | string | Event or task type discriminator |
+| `version` | string | Schema version (`"1"`, `"2"`) |
+| `source` | string | Service that emitted this event |
+
+#### Do
+
+- Propagate `correlation_id` through every integration event, background task, and log statement in the chain.
+- Use structured logging — log `correlation_id` and `causation_id` as indexed fields, not embedded in a message string.
+
+#### Don't
+
+- Generate a new `correlation_id` mid-chain.
+- Omit `causation_id` — without it, the causal graph is incomplete.
+
+---
+
+## 5. References
+
+- Evans, E. (2003). *Domain-Driven Design: Tackling Complexity in the Heart of Software*. Addison-Wesley.
+- Richardson, C. (2018). *Microservices Patterns*. Manning. *(Outbox pattern, Saga, CQRS)*
+- Hohpe, G. & Woolf, B. (2003). *Enterprise Integration Patterns*. Addison-Wesley.
+- Kleppmann, M. (2017). *Designing Data-Intensive Applications*. O'Reilly. *(CDC, at-least-once delivery, idempotency)*
+- Stopford, B. (2018). *Designing Event-Driven Systems*. O'Reilly.
diff --git a/docs/best-practices.md b/docs/best-practices.md
new file mode 100644
index 0000000..3102646
--- /dev/null
+++ b/docs/best-practices.md
@@ -0,0 +1,559 @@
+# Best Practices — DDD & Hexagonal Architecture
+
+> **Key points reference.** This document summarises the design rules and principles behind the seedwork packages. It is intentionally concise — each section captures the essential constraints. For deeper context, see the [references](#5-references) at the end.
+
+---
+
+## 1. Hexagonal Architecture — Fundamental Rules
+
+The dependency rule is the single constraint that everything else follows from:
+
+```text
+Domain ← Application ← Infrastructure
+```
+
+- **Domain layer** — pure business logic. No framework, no database, no HTTP. Depends on nothing outside itself.
+- **Application layer** — orchestration and use cases. Depends on domain types and abstract ports (interfaces/protocols). No concrete infrastructure.
+- **Infrastructure layer** — implements ports. Depends inward. Never imported by domain or application.
+
+**Ports are owned by the inner layer.** A port is an interface or protocol defined in domain or application. The adapter that implements it lives in infrastructure. The inner layer dictates the contract; the outer layer conforms.
+
+### Do
+
+- Keep the dependency arrow always pointing inward.
+- Define ports in the layer that needs them, not the layer that implements them.
+
+### Don't
+
+- Import infrastructure types from application or domain.
+- Let framework annotations or database types leak into domain objects.
+
+---
+
+## 2. Domain Components
+
+### 2.1 Entity
+
+An entity has **identity**. Two entity instances with the same ID are the same entity regardless of their other fields. Equality is identity-based, not structural.
+
+#### Key points
+
+- Identity is established at construction and never changes.
+- State changes only through explicit behaviour methods — no public setters.
+- Behaviour methods return new instances (immutability). They never mutate `this`/`self`.
+- Invariants are enforced in the constructor and behaviour methods. Invalid state must be impossible to construct.
+
+#### Do
+
+- Model meaningful domain operations as named methods (`confirm()`, `suspend()`, `assign()`).
+- Throw a `DomainError` subclass when an invariant is violated.
+
+#### Don't
+
+- Expose setters or public mutable fields.
+- Put orchestration or persistence logic inside an entity.
+
+---
+
+### 2.2 Value Object
+
+A value object has **no identity**. Two instances with the same field values are equal. They are always immutable.
+
+#### Key points
+
+- Equality is structural (all fields must match).
+- Validated at construction — a value object is either valid or it cannot exist.
+- Replaces primitive obsession: prefer `Money`, `Email`, `DateRange` over raw scalars.
+- Self-contained: carries its own validation and behaviour (e.g. `Money.add(other)`).
+
+#### Do
+
+- Use value objects for any concept defined entirely by its attributes.
+- Validate inside the constructor — throw `DomainError` on invalid input.
+
+#### Don't
+
+- Use a value object for a concept that needs to be tracked over time — that is an entity.
+- Share mutable state between value objects.
+
+---
+
+### 2.3 Aggregate
+
+An aggregate is a **consistency boundary**. It groups one or more entities and value objects under a single root (the aggregate root) that enforces all invariants that span the group.
+
+#### Key points
+
+- The aggregate root is the only entry point. External code never modifies inner entities directly.
+- All state changes go through behaviour methods on the root.
+- Reference other aggregates by ID only — never hold object references across aggregate boundaries.
+- Behaviour methods return new instances and append domain events.
+- Keep aggregates small. A large aggregate with many inner entities is almost always a sign of a missing bounded context boundary.
+
+#### Do
+
+- Design the aggregate around the business invariants it must enforce.
+- Use `reconstitute` (or equivalent) static factory when loading from persistence — no events.
+
+#### Don't
+
+- Put infrastructure or application logic inside the aggregate.
+- Let an aggregate grow to cover unrelated concepts — split it.
+
+---
+
+### 2.4 Domain Event
+
+A domain event is an **immutable fact** that something meaningful happened within the domain.
+
+#### Key points
+
+- Named in past tense, using business language: `OrderConfirmed`, `PaymentProcessed`, `CustomerUpgradedToPremium`.
+- Emitted by the aggregate root after a state change.
+- Payload contains primitives only — no value object instances, no aggregate references.
+- Processed **synchronously within the same transaction**. Domain event handlers run before the transaction commits.
+- Scope: in-process, same bounded context.
+
+#### Do
+
+- Apply the business-language test: would a non-technical stakeholder understand this event name without explanation?
+- Keep payloads minimal and serializable.
+
+#### Don't
+
+- Name events after technical operations (`UserUpdated`, `RecordCreated`).
+- Use domain events to communicate across bounded context boundaries — use integration events for that.
+- Embed value objects or entities in the payload.
+
+---
+
+### 2.5 Domain Service
+
+A domain service encapsulates **domain logic that does not naturally belong to a single aggregate or value object**.
+
+#### Key points
+
+- Stateless — no mutable fields, no persistence.
+- Operates on domain objects passed to it as arguments.
+- Justified when: the logic involves multiple aggregates, or requires a read from a domain port to make a domain decision.
+- Should be rare. If most of your logic lives in services rather than aggregates, you have an anemic domain model.
+
+#### Do
+
+- Keep domain services slim and focused on a single coordination concern.
+- Name them after the domain concept they represent, not the operation (`PricingPolicy`, `TransferAuthority`).
+
+#### Don't
+
+- Use domain services as a dumping ground for logic that belongs in aggregates.
+- Inject infrastructure adapters into a domain service — use ports.
+- Make domain services stateful.
+
+---
+
+### 2.6 Repository
+
+A repository is the **outbound domain port for aggregate persistence**. It abstracts the storage mechanism behind a collection-like interface.
+
+#### Key points
+
+- Only identity-based operations: `find_by_id`, `save`, `delete_by_id`.
+- Returns full domain aggregates — not DTOs, not raw rows.
+- One repository per aggregate root.
+- The interface is defined in the domain layer; the implementation lives in infrastructure.
+
+#### Do
+
+- Keep the repository interface minimal — only what the domain actually needs.
+- Return `None` from `find_by_id` when not found — let the caller decide.
+
+#### Don't
+
+- Add query methods to the domain repository (`findByEmail`, `findByStatus`). Use a separate read repository in the application layer.
+- Implement business logic inside the repository.
+- Let the repository return partial aggregates or raw data structures.
+
+---
+
+### 2.7 Domain Ports (Outbound)
+
+Domain ports are **abstract interfaces owned by the domain or application layer** that allow inner layers to interact with the outside world without depending on it.
+
+#### Key points
+
+- Defined in the layer that needs them (domain or application). Implemented in infrastructure.
+- **Read ports** — no side effects. Can be called outside of a transaction. Examples: ACL adapters that query external services, read repositories.
+- **Write ports** — have side effects. Must participate in the transaction if they mutate state. Examples: `Repository.save`, `IntegrationEventPublisher` (via outbox), `TaskScheduler` (via outbox).
+- The Anti-Corruption Layer (ACL) pattern translates external models into domain models at the port boundary. The adapter calls the external service; a parser maps the response to domain types.
+
+#### Do
+
+- Define ports at the granularity of a single concern.
+- Use ACL parsers to isolate external model changes from the domain.
+
+#### Don't
+
+- Mix read and write responsibilities in a single port.
+- Let external data structures cross the port boundary into the domain.
+
+---
+
+## 3. Application Components
+
+### 3.1 Command and CommandHandler
+
+A command expresses **intent to change state**. The handler orchestrates the operation.
+
+#### Key points
+
+- One command per write use case.
+- Commands are plain frozen dataclasses — they carry the data needed to perform the operation.
+- The handler's responsibility is orchestration only: load aggregate → call domain method → save. No business logic in handlers.
+- The handler returns nothing — outcome is communicated via `Result` by the command bus.
+
+#### Do
+
+- Keep handlers thin. If a handler contains conditionals over domain state, that logic probably belongs in the aggregate.
+
+#### Don't
+
+- Put business rules in the handler.
+- Return data from a command handler — use a query for that.
+
+---
+
+### 3.2 Query and QueryHandler
+
+A query is a **request for data with no side effects**.
+
+#### Key points
+
+- Returns `TResult | None` — a result or `None` when absent.
+- Query handlers are strictly read-only. No state changes, no command dispatching.
+- Use **read repositories** (ad-hoc ports defined in the application layer), not the domain repository. The domain repository loads full aggregates; queries often need projections.
+- Returns DTOs (plain data objects with primitive fields) — never domain entities.
+
+#### Do
+
+- Define a dedicated read port per query when the projection differs from the aggregate.
+- Return `None` for both not-found and unauthorized — do not reveal resource existence to unauthorized callers.
+
+#### Don't
+
+- Load a full aggregate in a query handler just to extract two fields.
+- Dispatch commands or trigger side effects from a query handler.
+- Return domain aggregates as query results.
+
+---
+
+### 3.3 Unit of Work
+
+The Unit of Work defines the **transaction boundary** for a command execution.
+
+#### Key points
+
+- Wraps the entire command handler execution: aggregate load, domain method call, save, domain event dispatch — all inside one transaction.
+- On commit: all changes persist and domain event handlers execute.
+- On rollback: no state persisted, no events published.
+- The transaction boundary is the command. Never let a transaction span multiple commands.
+
+#### Do
+
+- Use `TransactionalCommandBus` to apply the Unit of Work transparently — handlers never see it.
+- Ensure domain event handlers run inside the same transaction.
+
+#### Don't
+
+- Open a transaction manually in a handler.
+- Span a transaction across multiple aggregate roots or multiple commands.
+
+---
+
+### 3.4 Application Ports (Outbound)
+
+Two outbound ports at the application layer manage **eventual side effects**:
+
+#### `IntegrationEventPublisher`
+
+- Publishes integration events to external consumers via the outbox pattern.
+- The publish call writes to the outbox **inside the current transaction**. Actual delivery to the broker happens after commit, asynchronously.
+- The application layer depends on the interface; infrastructure provides the outbox-backed implementation.
+
+#### `TaskScheduler`
+
+- Schedules background tasks for the same service via the outbox pattern.
+- Same transactional guarantee: scheduling writes to the outbox inside the transaction.
+
+#### Key points
+
+- Both ports are write ports but their delivery is eventual — they do not block the command response.
+- Transactional atomicity is guaranteed by the outbox: either the aggregate saves and the outbox record is written, or neither happens.
+- The application layer is agnostic to the delivery mechanism (broker, worker queue, etc.).
+
+#### Do
+
+- Call `IntegrationEventPublisher.publish` and `TaskScheduler.schedule` from command handlers or domain event handlers — never from the domain layer.
+
+#### Don't
+
+- Assume synchronous delivery.
+- Publish directly to a broker from a handler — always go through the port.
+
+---
+
+### 3.5 Result
+
+`Result` is the **value-based error contract for commands**.
+
+#### Key points
+
+- Two states: `ok()` (success) and `failed(errors)` (failure with a list of `ResultError`).
+- `ResultError` carries a `code` (machine-readable) and a `description` (human-readable).
+- The command bus catches `DomainError` thrown by handlers and converts them to `Result.failed`. Handlers never return `Result` directly.
+- Infrastructure failures (timeouts, connection drops) propagate as exceptions — they are not wrapped in `Result`.
+
+#### Do
+
+- Inspect `Result` at the entry point (controller, API handler) to decide the HTTP response.
+- Use error `code` values for programmatic branching; use `description` for logging and user messages.
+
+#### Don't
+
+- Throw `DomainError` and catch it in the handler — let the bus handle the conversion.
+- Wrap infrastructure exceptions in `Result`.
+
+---
+
+### 3.6 Error Handling
+
+| Error type | Origin | Handling |
+|---|---|---|
+| `DomainError` | Aggregate / domain service | Caught by command bus → `Result.failed` |
+| Infrastructure exception | Repository, external adapter | Propagates — do not catch or wrap |
+
+#### Key points
+
+- The entry point (controller) only needs to handle `Result` for domain failures. It never catches `DomainError`.
+- Infrastructure exceptions bubble up to a global error handler.
+- Never use exceptions for flow control within the domain — that is what `DomainError` → `Result` is for.
+
+#### Do
+
+- Define a `DomainError` subclass for each distinct business rule violation with a stable `code`.
+
+#### Don't
+
+- Catch `DomainError` in handlers.
+- Return `Result.failed` for infrastructure failures.
+- Use generic exception types for domain errors.
+
+---
+
+### 3.7 Operation Flows
+
+#### Write operation
+
+```mermaid
+sequenceDiagram
+ participant API
+ participant Bus as CommandBus stack
(transaction → coordinator → registry)
+ participant Handler as CommandHandler
+ participant Agg as Aggregate
+ participant PubRepo as DomainEventPublishingRepository
+ participant DeferredBus as DeferredDomainEventBus
+ participant EH as DomainEventHandler
+ participant IEP as IntegrationEventPublisher
+ participant TS as TaskScheduler
+
+ Note over API: create command
(domain values validated in __post_init__)
+ API->>Bus: dispatch(command)
+ Note over Bus: 1. begin transaction
+ Bus->>Handler: handle(command)
+ Handler->>PubRepo: find_by_id(id)
+ PubRepo-->>Handler: aggregate
+ Handler->>Agg: behaviourMethod(...)
+ Agg-->>Handler: new aggregate + domain events
+ Handler->>PubRepo: save(aggregate)
+ PubRepo->>DeferredBus: publish(domainEvents)
+ Note over DeferredBus: events buffered — not dispatched yet
+ Handler-->>Bus: (returns)
+ Note over Bus: 2. dispatch deferred events
(DomainEventCoordinatorCommandBus)
+ Bus->>DeferredBus: dispatch()
+ DeferredBus->>EH: handle(event)
+ EH->>IEP: publish(integrationEvent)
+ Note over IEP: written to outbox (same TX)
+ EH->>TS: schedule(task)
+ Note over TS: written to outbox (same TX)
+ EH-->>Bus: (returns)
+ Note over Bus: 3. commit transaction
+ Bus-->>API: Result
+```
+
+#### Read operation
+
+```mermaid
+sequenceDiagram
+ participant API
+ participant Bus as QueryBus
+ participant Handler as QueryHandler
+ participant ReadRepo as ReadRepository
+
+ Note over API: create query
(domain values validated in __post_init__)
+ API->>Bus: ask(query)
+ Bus->>Handler: handle(query)
+ Handler->>ReadRepo: find projection
+ ReadRepo-->>Handler: DTO | None
+ Handler-->>Bus: TResult | None
+ Bus-->>API: TResult | None
+```
+
+---
+
+## 4. Domain Events · Integration Events · Background Tasks
+
+This is the most consequential design decision in a service. Using the wrong mechanism introduces either excessive coupling or unnecessary complexity.
+
+### 4.1 Domain Event
+
+A domain event is an **in-process fact within the same bounded context**.
+
+| | |
+|---|---|
+| **Scope** | In-process, same bounded context |
+| **Consistency** | Strong — synchronous, within the same transaction |
+| **Delivery** | Guaranteed — in-process method call |
+| **Audience** | Other aggregates within the same bounded context |
+
+#### Key points
+
+- Processed by `DomainEventHandler` implementations registered on the `DomainEventBus`.
+- Handlers run within the same transaction as the command — if the transaction rolls back, their effects are not persisted.
+- The `DomainEventPublishingRepository` decorator publishes events automatically after `save`. Handlers are unaware of the publishing mechanism.
+- The deferred bus (`DeferredDomainEventBus`) buffers events and dispatches them after the command handler completes but before the transaction commits. Idempotent by event ID — saving the same aggregate twice in one transaction does not duplicate events.
+
+#### Do
+
+- Use domain events for intra-bounded-context reactions: updating a read model, enforcing a cross-aggregate invariant reactively, triggering a follow-up operation within the same service.
+
+#### Don't
+
+- Use domain events to notify other services or bounded contexts.
+- Perform I/O with side effects outside the transaction in a domain event handler.
+
+---
+
+### 4.2 Integration Event
+
+An integration event is a **public notification that something relevant happened**, intended for consumers outside the bounded context.
+
+| | |
+|---|---|
+| **Scope** | Cross-service, cross-bounded-context |
+| **Consistency** | Eventual — outbox + message broker |
+| **Delivery** | At-least-once (idempotent consumers required) |
+| **Audience** | Other services / bounded contexts |
+
+#### Key points
+
+- Published via `IntegrationEventPublisher`. The outbox pattern guarantees atomicity: the event record is written in the same transaction as the aggregate.
+- Carries `type`, `version`, `correlation_id`, and optionally `causation_id`. These fields are mandatory because integration events are a **public, versioned contract**.
+- Schema evolution: add optional fields (backwards-compatible). Never rename, remove, or change the type of existing fields.
+- Consumers must be idempotent — at-least-once delivery is the default guarantee of any message broker.
+
+#### Do
+
+- Publish integration events from domain event handlers (reacting to a domain fact).
+- Version integration events from day one. Even `v1` is a version.
+- Include `correlation_id` from the originating command.
+
+#### Don't
+
+- Assume that publishing an integration event means the consumer received it immediately.
+- Change the schema of a published integration event without introducing a new version.
+- Use integration events for work that stays within the same service — use a background task instead.
+
+---
+
+### 4.3 Background Task
+
+A background task is **deferred work within the same service**.
+
+| | |
+|---|---|
+| **Scope** | Same service |
+| **Consistency** | Eventual — outbox + internal worker |
+| **Delivery** | At-least-once (idempotent handlers required) |
+| **Audience** | A `TaskHandler` within the same service |
+
+#### Key points
+
+- Scheduled via `TaskScheduler`. Like `IntegrationEventPublisher`, it writes to the outbox inside the transaction.
+- Use for: long-running operations, retryable work, resource-intensive processing, or anything that should not block the command response.
+- The key distinction from integration events: a background task stays within the service and is processed by a `TaskHandler` in the same codebase. No external consumer.
+- Handlers must be idempotent — the worker may deliver the task more than once.
+
+#### Do
+
+- Include `correlation_id` (and `causation_id` when applicable) in every task.
+- Design `TaskHandler` implementations to be idempotent by task ID.
+
+#### Don't
+
+- Use background tasks to communicate with other services — that is an integration event.
+- Perform fire-and-forget work without the outbox guarantee — if the service crashes between scheduling and execution, the task would be lost.
+
+---
+
+### 4.4 Decision Guide
+
+```text
+Did something meaningful happen in the domain?
+└── YES → Emit a Domain Event
+ │
+ ├── Does another bounded context need to know?
+ │ └── YES → Also publish an Integration Event
+ │ (from a DomainEventHandler)
+ │
+ └── Does work need to happen asynchronously within this service?
+ └── YES → Schedule a Background Task
+ (from a DomainEventHandler)
+```
+
+#### Concrete examples
+
+| Scenario | Mechanism |
+|---|---|
+| Order confirmed → update order read model | Domain Event |
+| Order confirmed → notify shipping service | Integration Event |
+| Order confirmed → generate PDF invoice (slow) | Background Task |
+| Payment processed → update account balance | Domain Event |
+| Payment processed → notify customer service | Integration Event |
+| User registered → send welcome email (retryable) | Background Task |
+
+---
+
+### 4.5 Cross-cutting Concerns
+
+**Naming** — all three use past tense and business language. The name must be meaningful to a non-technical stakeholder without explanation.
+
+**Idempotency** — at-least-once delivery is the default. Every domain event handler, integration event consumer, and task handler must handle duplicates. Use the event/task `id` as the deduplication key.
+
+**Traceability** — propagate `correlation_id` from the originating command through every downstream event and task. Use `causation_id` to record the ID of the event or command that directly caused this one. These two fields are what make distributed traces reconstructable.
+
+---
+
+## 5. References
+
+- Evans, E. (2003). *Domain-Driven Design: Tackling Complexity in the Heart of Software*. Addison-Wesley. [1]
+- Vernon, V. (2013). *Implementing Domain-Driven Design*. Addison-Wesley. [2]
+- Robert C. Martin, *Clean Architecture: A Craftsman's Guide to Software Structure and Design* [3]
+- .NET Microservices: *Architecture for Containerized .NET Applications* [4]
+- Harry Percival & Bob Gregory, *Architecture Patterns with Python* [5]
+- Cockburn, A. (2005). *Hexagonal Architecture*. [6]
+
+[1]: https://www.amazon.es/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215
+[2]: https://www.amazon.es/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577
+[3]: https://www.amazon.es/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164
+[4]: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/
+[5]: https://www.oreilly.com/library/view/architecture-patterns-with/9781492052197/
+[6]: https://alistair.cockburn.us/hexagonal-architecture/