Choosing the shape
The first decision is whether the caller needs an answer now. Almost every other decision follows from it, and most integration pain comes from having answered it wrongly and built around the mistake.
Point-to-point, when there are two of you
A direct call is the simplest thing that works, and simplicity is worth a great deal. The cost is coupling: the caller needs the callee up, addressable and fast.
It stops scaling as a design at about the fourth participant, when the number of connections starts growing faster than the team.
Use it when two systems, synchronous need, and an SLA you both control.Queue for work, topic for facts
A queue distributes work to exactly one consumer — use it when something must happen once. A topic broadcasts an event to every interested subscriber — use it when something has happened and others may care.
Naming follows the same split. Queues take commands (process-payment); topics take past-tense events (payment-settled). If you cannot name it in the past tense, it is probably a command.
Async request-reply
The caller needs an answer, but the work takes longer than a request should live. Accept the request, return 202 with a status URL, and let the caller poll or subscribe.
The trap is designing the status resource as an afterthought. It needs its own identity, a terminal state, and a retention policy — otherwise you are storing every job forever to answer a question nobody asks twice.
Use it when work takes longer than a few seconds but the caller still needs the outcome.Making it reliable
Every one of these exists because a message was lost, duplicated, or processed twice with real financial consequences. None of them are theoretical.
The two hardest guarantees in integration: that a message is sent if and only if the database change committed, and that processing it twice is harmless. The outbox handles the first. Idempotency handles the second. You need both.
Transactional outbox
Writing to your database and publishing to a broker are two separate systems, and there is no transaction spanning them. Commit then publish, and a crash between the two loses the message. Publish then commit, and a rollback leaves a message about something that never happened.
The outbox removes the gap: write the message into an outbox table inside the same transaction as the business change, then have a separate process read that table and publish. The database is the single source of truth about what happened.
Use it when a published event must exactly match committed state — which is any event carrying financial or legal weight.Idempotency, by key not by hope
At-least-once delivery is what brokers actually give you. Redelivery after a consumer crash is normal operation, not an error case.
Give every message a stable id, record processed ids, and make handling a repeat a no-op. “We check if it already exists first” is not idempotency — it is a race condition with extra steps, unless the check and the write are in one transaction or enforced by a unique constraint.
Use it when always, on every consumer. Assume every message arrives twice.Retry, then dead letter
Retry transient failures with exponential backoff and jitter — without jitter, every consumer retries in lockstep and you have rebuilt the thundering herd.
Cap the attempts and dead-letter the rest. A dead letter queue nobody monitors is just a slower way of losing messages, so alert on depth, and build the replay path before you need it at 2am.
Use it when always. The default of infinite retry blocks the queue behind one poison message.Claim-check for large payloads
Brokers have message size limits and get slow well before them. Put the payload in blob storage, put the reference in the message.
It also fixes a privacy problem: the message bus stops being a place personal data accumulates in a retention policy nobody wrote.
Use it when payloads exceed a few hundred KB, or contain data you would rather not have sitting in a broker.Consistency across services
Saga instead of a distributed transaction
Two-phase commit across services is not available to you in practice. A saga replaces it with a sequence of local transactions, each with a compensating action if a later step fails.
Compensation is not rollback. Refunding a payment is not the same as the payment never happening, and the difference shows up in statements, audit logs and customer emails. Design compensations as real business operations.
Use it when a business process spans services and partial completion is unacceptable.Choreography or orchestration — pick one per process
Choreography has each service react to events with no central coordinator: loosely coupled, and very hard to answer “where is order 12345 stuck?”. Orchestration has a coordinator drive the steps: easy to reason about, and a component everything depends on.
Mixing them within a single process is the worst outcome — half the flow is visible in a state machine and half is implicit in subscriptions.
Use it when orchestration for processes with a business owner who asks about status; choreography for genuinely independent reactions.Edges you do not control
Webhooks: verify, queue, then work
An inbound webhook is an untrusted HTTP request from a system with its own retry policy. Verify the signature, return 2xx immediately, and do the actual work off a queue.
Doing the work inline means the sender's timeout dictates your processing budget, and their retry doubles your load exactly when you are slow.
Use it when receiving events from any third party — payment providers, source control, CRM.Anti-corruption layer
When integrating with a system whose model you disagree with — a legacy ERP, a vendor API, an acquisition — translate at the boundary rather than letting its concepts spread through your domain.
The cost is a mapping layer to maintain. The benefit is that replacing that system later is a project rather than a rewrite.
Use it when the other system's model would otherwise leak into yours, especially if you expect to replace it.Where to read more
- JavaScript design patterns ↗ — the Gang of Four catalogue, for the patterns inside a service
- Azure Architecture Center: cloud design patterns ↗
- Azure Service Bus messaging ↗
- The Azure architecture guide ↗ — where these sit in a wider design