Integration Architecture
How components communicate across boundaries: message style, data flow direction, and failure handling.
This skill complements [Unfolding Architecture](../unfolding-architecture/SKILL.md), which covers the decision of whether to use messaging at all (Communication dimension, Levels 0→1→2). Once you've decided to communicate across boundaries, this skill guides how.
Tradeoff: Command vs. Event Messages
Commands couple sender to receiver: the sender knows who to call and what to ask for. Events invert the dependency: receivers must know the sender's domain to interpret what happened. In more complex messaging patters, when several, interdependent messages are sent, the dependency is even stricter: the sender of commands must know about the state of the receiver; with events it's the other way around. Neither is strictly better.
Prefer Commands when:
- There is exactly one receiver
- Sender needs confirmation the action was performed
- Traceability and explicit flow matter
- The operation is imperative ("do this")
Prefer Events when:
- Multiple independent consumers react to the same thing
- Adding consumers should not require changing the sender
- Sender should not know about side effects
- The message describes something that happened ("this occurred")
Key insight: Events don't remove coupling, they move it. The receiver now needs to understand the sender's context to react. This is worthwhile when senders change less often than the set of receivers.
When introducing cross-boundary communication, use AskUserQuestion with a code-based recommendation:
- Question: "Should this be a command or an event?"
- Options: Put the recommended option first with "(Recommended)". Use the option
description
to explain why based on the code — e.g., "The method name orderCompleted and the lack of a return value suggest a notification, not a directive" or "There is exactly one receiver (PaymentService) that needs to confirm the action."
- "Let's discuss" — elaborate on the trade-offs before deciding
- If the code signals are ambiguous, don't recommend — present all options neutrally.
Request-Reply
A hybrid pattern: the sender sends a command and waits for a response, but communication is asynchronous (via a broker with a reply queue, not a direct method call). Useful when you need the decoupling benefits of messaging but the sender still requires a result.
Prefer Request-Reply when:
- The sender needs a result but should not be coupled to the receiver's deployment
- Temporal decoupling is needed (the reply may arrive later)
- Load leveling or routing through a broker is required
Prefer direct commands when:
- Latency matters and a synchronous call is acceptable
- The reply queue infrastructure would add complexity without clear benefit
Tradeoff: Push vs. Pull
Who initiates data flow.
Prefer Pull when:
- Consumer only needs data on-demand
- Consumer controls timing and rate
- Request/response is sufficient
- Simpler infrastructure (HTTP, DB queries)
Prefer Push when:
- Consumers need data as soon as it's available
- Polling would create unnecessary load
- Multiple consumers need the same data
- Producer knows best when data is ready
Key insight: Push shifts backpressure responsibility to the consumer. Pull is simpler but can miss real-time needs or waste resources on empty polls.
Backpressure
When a producer pushes faster than a consumer can process, work piles up. Backpressure is how the consumer signals the producer to slow down (or how the system absorbs the mismatch).
Common strategies:
- Bounded queues: reject or block when full — producer discovers the limit
- Rate limiting: consumer controls ingestion rate explicitly
- Credit-based flow: consumer grants the producer permission to send N messages per timeframe
- Competing consumers: scale out consumers to match producer throughput
Choose a strategy before going to production with push-based messaging. Unbounded queues are not a strategy — they defer the problem until memory runs out.
Dimension: Reliability
How communication handles failures. This dimension progresses under pressure, following the unfolding principle: start at Level 0, unfold only when concrete problems force the change.
| Level | Style | Description | |-------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 0 | Fire and forget | Send and assume success. No retries. Simple, appropriate when loss is acceptable or communication is in-process. | | 1 | Retry + idempotency | Retry on failure; receivers are idempotent so duplicates are harmless. At-least-once delivery. Techniques: deduplication (message IDs), upserts, deterministic operations. | | 2 | Transactional guarantees | Strongest guarantees, highest complexity. See techniques below. |
When choosing a reliability level, use AskUserQuestion with a code-based recommendation:
- Question: "What reliability level does this communication need?"
- Options: Put the recommended option first with "(Recommended)". Use the option
description
to explain why based on the code — e.g., "This is an in-process call; failure means process crash anyway, so fire-and-forget is appropriate" or "This payment flow has business consequences on message loss; retry + idempotency is the minimum."
- "Let's discuss" — elaborate on the trade-offs before deciding
- If the code signals are ambiguous, don't recommend — present all options neutrally.
Unfold to Level 1 when:
- Communication crosses process or network boundaries,
do transient failures (network blips, restarts) are to be expected
- Message loss has business consequences
Do NOT unfold to Level 1 when:
- Communication is in-process where failure = process crash anyway
- The operation is naturally idempotent and loss is acceptable for the business
Unfold to Level 2 when:
- Duplicates have business consequences that idempotency cannot absorb
- An operation must atomically update local state AND send a message (outbox pattern)
- Multiple services must coordinate a consistent outcome (saga)
Do NOT unfold to Level 2 when:
- Idempotency is cheap to implement (prefer Level 1)
- The operation is naturally idempotent (e.g., setting a value, not incrementing)
- Only one service owns the data being changed
Level 2 Techniques
Transactional outbox: Instead of sending a message directly (which can fail independently of the local transaction), write the message to an outbox table in the same database transaction as the business data change. A separate process reads the outbox and publishes the messages. This guarantees that the local state change and the message are atomic — either both happen or neither does.
Saga: A sequence of local transactions across multiple services, each publishing an event or command that triggers the next step. If a step fails, compensating transactions undo the preceding steps. Sagas trade atomicity for availability — the system is eventually consistent, and you must design compensating actions for every step that can fail.
Testing Distributed Failures
Integration code fails in ways that unit tests for business logic never exercise: brokers restart, messages arrive out of order, schemas drift between producer and consumer, compensation logic triggers under unexpected conditions. The scopes below are independent — pick t