An online shop needs a delivery price before it can show the customer a final total. The same shop needs to send a receipt after an order is accepted. Both involve one part of the application asking another part to do something, but the customer is waiting for very different outcomes.
The delivery price belongs in the current conversation. The receipt can usually arrive a little later, provided the application has reliably recorded that it must be sent. Choosing how services communicate starts with that difference, rather than with a rule that every service must use either HTTP or a queue.
This article follows a small shop through both approaches. We will look at successful calls, slow services, lost responses, delayed messages and the information a user needs when work continues after their request ends.
Introduction
Our shop has a web application, a delivery service and a receipt worker. The delivery service calculates available delivery options. The receipt worker creates and sends order confirmations. These components may run on different servers, which means communication can fail independently of the code performing the work.
An HTTP request is a familiar way to ask another service for a response. A message queue adds a holding place between a producer, which submits work, and a consumer, which processes it. That holding place changes when the components need to be available and who owns unfinished work.
Neither approach automatically makes an operation reliable. We still need to define what success means, when a retry is safe and how accepted work is recovered. The aim is to choose a communication pattern whose promises match the operation.
Ask What the Caller Needs Before Continuing
When the customer chooses an address, checkout needs the delivery options before showing the next step. A direct request to the delivery service is a natural fit. The response contains the answer the customer is waiting to use.
After the order is accepted, the customer normally does not need to wait for an email provider to confirm delivery before seeing the order number. The shop can record a receipt task and let a worker complete it separately.
This gives us a useful first question: does the caller need the result now, or does it need a reliable promise that the work will happen later? “Now” does not mean instantly. It means within the current interaction's useful lifetime.
Some operations need both behaviours. A report request can immediately return an accepted job identifier, while the completed report becomes available later. The initial response answers whether the job was accepted; it does not claim that the report has already been generated.
Microsoft's interservice communication guidance distinguishes communication styles without prescribing one protocol for every interaction. The business dependency is the useful starting point for the design.
Understand the Direct HTTP Path
In a direct request, the web application sends a request to the delivery service and waits for its response. The delivery service validates the input, calculates options and returns data or an error.
Browser -> Shop API -> Delivery API
Browser <- Shop API <- Delivery options
The shop needs the delivery service to be reachable during that interaction. It also inherits enough of the delivery service's delay to affect the customer's experience. If the delivery call takes three seconds, checkout cannot finish that step in one second while still waiting for the answer.
The code may use await so it does not block a thread while the network operation is pending. That is a programming technique, not a change to the business dependency. The customer request still depends on the downstream response arriving.
HTTP provides status codes, headers and a request-response structure, but those do not define the complete business contract. A 200 response with an empty list might mean no delivery options exist. A timeout means the caller did not receive an answer in time. Those outcomes should not be treated as equivalent.
Understand the Queued Work Path
For receipts, the shop submits a message describing the work. A broker stores and distributes messages according to its configuration. A receipt worker receives one, loads the required order information and performs the task.
Shop API -> Durable receipt request -> Message queue
Receipt worker <- Message queue
Receipt worker -> Email provider
The worker can be temporarily unavailable while new tasks accumulate. Once it returns, it resumes processing the backlog. The shop and worker no longer need to complete their parts at the same instant.
That flexibility depends on the queue's actual storage and delivery guarantees. A message placed in a process's memory disappears if that process crashes. A durable broker also needs appropriate configuration, acknowledgements and operational capacity; its name alone is not a guarantee.
The producer usually receives confirmation that the broker accepted responsibility for the message. It does not receive proof that the email was sent. RabbitMQ's acknowledgement and publisher-confirm documentation distinguishes the producer-to-broker confirmation from the consumer's acknowledgement after processing.
For the shop, customer-facing state should therefore come from the durable order and receipt workflow, rather than interpreting “message sent” as “receipt delivered”.
Separate Commands from Events
A command asks for an action: SendReceipt, GeneratePreview or ReserveStock. It normally has an intended owner that decides whether and how to perform the requested work.
An event describes something that happened: OrderAccepted, PreviewGenerated or StockReserved. Several independent applications may have reasons to react to the same fact.
The names matter because they tell a reader what is already true. A message called ReceiptSent should not be published when the shop merely wants someone to send a receipt. Otherwise another service can make decisions based on a fact that has not happened yet.
A queue with competing receipt workers distributes one work item among those workers. A publish-subscribe arrangement gives separate subscriptions their own copies, allowing invoicing and analytics to react independently to OrderAccepted.
Do not expect three workers reading the same work queue to behave like three independent subscribers. They usually share the work rather than each receiving every message. The communication contract should say whether the goal is distributing jobs or notifying several applications.
Compare the User-Visible Outcomes
With a synchronous delivery request, the interface can show delivery choices or explain that the quote is temporarily unavailable. The outcome belongs to the current page transition.
With a queued report, the interface needs a way to represent progress. It might show pending, processing, completed or failed, with a link to retrieve the result when ready. A loading spinner that remains tied to one long HTTP connection defeats much of the benefit.
An accepted operation should have a stable identifier. The client can use it after a refresh or connection loss instead of starting another copy because its first browser request disappeared.
The asynchronous request-reply pattern describes an initial accepted response followed by a separate way to obtain status or results. Polling is one option; a notification can improve responsiveness, but durable status remains useful when notifications are missed.
Keep progress states honest. “Complete” should mean the promised result exists and is available through the expected access path. A worker finishing its calculation is not sufficient if uploading the report or publishing its download reference still failed.
Work Through a Lost HTTP Response
Suppose checkout asks inventory to reserve one item. Inventory commits the reservation, but the network connection breaks before checkout receives the response. The caller sees a failure even though the requested action succeeded.
Repeating the request may create another reservation unless the operation supports safe repetition. This is why a timeout is an unknown outcome rather than proof that the server did nothing.
Use a stable operation identifier for the logical reservation. Inventory records that identifier with the request details and result. A repeated request with the same identifier can return the original outcome instead of reserving another item.
If the same identifier arrives with different quantities, the service should reject the mismatch. Otherwise an identifier intended to prevent duplicates becomes an ambiguous instruction that can change meaning between attempts.
The caller may also query operation status. That is useful when an important side effect cannot be repeated safely or when the downstream provider exposes a dedicated status mechanism. The choice depends on the receiver's contract, not merely on whether the transport was HTTP.
Work Through a Lost Queue Acknowledgement
A receipt worker sends the email and then crashes before acknowledging its message. The queue later delivers that message again. From the broker's perspective, the first worker never confirmed completion.
This is the queued version of the same uncertainty. Delivery retries help avoid losing work, but they can repeat work whose effect already happened. The consumer must recognise the logical operation and handle that possibility.
If the email provider supports a suitable idempotency key, the worker can reuse it across attempts. Otherwise the application may need a provider status check or accept a documented duplicate risk for this particular notification. Storing a local “sent” flag cannot atomically cover an unrelated provider unless their protocols support that coordination.
RabbitMQ's reliability guide explains why confirmations and recovery can still produce duplicates. The general lesson is to design repeatable processing instead of assuming that reliable delivery means one execution of every business effect.
The same concern applies to payments, shipping labels and account provisioning. A broker can manage message ownership; the receiving application and external system determine how repeated effects are prevented or resolved.
Record Work Before Promising It
The shop should not commit an order, return success and only then try to remember that a receipt needs sending. A crash between those steps leaves an accepted order with no durable receipt task.
An outbox is a small table of messages waiting to be dispatched. The order change and outbox entry are written in the same database transaction. Either both commit or neither does.
A separate publisher reads the outbox and sends its messages to the broker. If the broker is temporarily unavailable, the publisher retries later. If confirmation is lost, it may resend the message, which is why the consumer still needs duplicate handling.
The outbox does not make the email provider part of the database transaction. It protects the narrower promise that accepted order state has a durable record of the required follow-up work.
This boundary is useful even in a modest application. A database table and a background publisher may be enough initially; the important property is that accepted work survives the request process disappearing.
A Queue Absorbs Bursts, Not Unlimited Demand
Imagine the shop receives 100 receipt tasks per second for one minute, while workers can complete 80 per second. During that burst, the backlog grows by roughly 1,200 tasks, ignoring variations and retries.
When arrivals fall to 40 per second, workers have about 40 tasks per second of spare capacity to clear the backlog. The simple estimate is another thirty seconds to catch up.
If arrivals remain at 100 forever, the queue continues growing. Adding storage delays the moment it fills but does not increase the rate at which receipts are sent. A queue is a buffer between different rates, not a source of processing capacity.
Measure the age of the oldest unfinished task as well as queue length. Ten large reports can represent more delay than a thousand tiny notifications. Users care about when their result arrives, not the number of rows in a broker dashboard.
Set admission limits and recovery plans before the backlog becomes too old to be useful. A delivery notification sent weeks after the order arrived may be technically processed and still fail the product's purpose.
Keep Direct Calls Within a Useful Budget
For a delivery quote, decide how long checkout can wait. Give the downstream call a deadline that leaves time for the rest of the request and a clear response to the customer.
Retry only where the operation is safe to repeat and another attempt can fit within the remaining budget. Waiting two seconds, then performing three more two-second attempts, is incompatible with a page that must respond within three seconds.
Avoid multiplying retries across the browser, gateway, shop API and delivery client. Each layer may believe it is helping while their combined attempts overwhelm a struggling service.
A temporarily unavailable optional recommendation service can produce an empty recommendation section if that is acceptable. A failed delivery quote should not silently become a zero-cost quote. The fallback must preserve the meaning of the operation.
These choices also apply to consumers calling external services. Moving the caller into a worker changes who waits, but it does not remove the need for deadlines, safe retries and limits on concurrent dependency calls.
Choose What the Message Carries
A message can contain a complete snapshot, a small command or a reference to authoritative data. Each choice changes what happens when processing is delayed.
A receipt may need the order's agreed prices and delivery address at the time it was placed. Loading the customer's current address hours later could produce a receipt describing different information. Store or reference the accepted order snapshot deliberately.
A preview refresh may instead want the latest document version. Its message can identify the document and requested version, while the worker checks whether a newer request has superseded it.
Keep messages compact. Large files usually belong in object storage with a stable authorised reference in the message. That reference must remain valid for the maximum processing and retry period.
Include identifiers that explain the work: operation identity, resource identity, tenant scope and message format version where needed. Avoid copying an entire HTTP request, access token or database entity graph into a queue simply because serialisation makes it easy.
Plan for Changes to the Contract
Services are often deployed at different times. A new producer can send a message while old consumers are still running, and a delayed message may reach a new consumer days after it was created.
Adding an optional field with a documented default is usually easier to roll out than renaming a required field or changing a number into a string. Consumers should validate required information without rejecting harmless additions unnecessarily.
The same compatibility concern exists for HTTP APIs. A caller expecting deliveryPrice cannot automatically understand a response that replaces it with a completely different structure. Versioning and coordinated rollout should follow the change's meaning.
Messages add a longer-lived compatibility obligation because old payloads can remain in backlogs and archives. Retiring an old parser immediately after deploying new producers may strand accepted work that has not been consumed yet.
Keep a few representative payloads as fixtures. Test old and new versions against the intended consumer versions, including missing optional fields and explicitly unsupported formats. Those examples make compatibility review concrete.
Keep Ordering Requirements Narrow
Some work must happen in sequence. A document should not publish an old preview after a newer one, and a shipment should not use an address update that arrived after dispatch was already committed.
First identify the scope of that order. Operations for one document may need coordination, while operations for unrelated documents can run independently. Requiring one global sequence can unnecessarily restrict the entire system.
A queue does not automatically preserve business completion order when several workers run concurrently or retry at different times. An earlier message may take longer than a later one, even if it was delivered first.
Version checks can make some out-of-order work harmless. A preview generated from document version seven can be rejected if version eight is already current. Other operations need an ordered consumer or an explicit workflow that waits for prerequisites.
Choose the protection from the business rule. Adding a queue solely to “get ordering” is incomplete unless the delivery, concurrency, retry and final-write behaviours all support the required sequence.
Understand What Becomes Easier and Harder
Direct HTTP makes simple request-response interactions easy to follow. The caller receives the result through the same conversation, and familiar tooling can inspect the request, response and timing.
It also creates availability and latency dependencies. A long chain of calls means the original request can fail because any required downstream component is unavailable or too slow.
Queues separate the producer's timing from the worker's timing and make backlogs explicit. They support independent scaling and recovery of deferred work, but require status tracking, duplicate handling, message evolution and operational ownership.
The broker itself becomes a dependency. Its storage, permissions and capacity need management, and a misconfigured queue can be just as disruptive as an unavailable HTTP endpoint.
Use the additional machinery when it solves a real timing, recovery or workload problem. A small read API does not become better merely because its request and response are wrapped in two queues with correlation identifiers.
Use Both Patterns in One Workflow
Checkout can request current delivery options over HTTP, validate the selected quote and commit the accepted order locally. The transaction also records the receipt and fulfilment intents in an outbox.
The response returns the order number and accepted state. Workers later perform fulfilment preparation and receipt delivery. Their outcomes update durable records that customer support and the user interface can inspect.
If the receipt worker stops, order acceptance can continue within the defined backlog capacity. If the delivery service stops, checkout may be unable to offer delivery choices, while users can still browse products and inspect existing orders.
This mixed design follows each operation's requirements rather than selecting one universal transport. It also makes failure boundaries understandable: delivery quotes block one interactive step, while receipt delays affect a later notification.
Document those boundaries next to the workflow. A future developer can then see why sending a receipt synchronously inside order acceptance would change both latency and availability, even if it reduces the number of components in their method.
Avoid Hiding Work Behind Fire-and-Forget Tasks
Starting an unawaited task after returning a response can resemble asynchronous messaging in a small demonstration. The application looks responsive, and the task often completes while the process stays healthy.
However, the task may retain disposed request services, fail without an observed result or disappear during a restart. There is no durable backlog that another worker can inspect and recover.
An in-process bounded queue can be appropriate for replaceable work such as refreshing a preview. Its acceptance contract should say that a crash may lose the hint and later demand will regenerate it.
For work the application promises to complete, put durability at the acceptance boundary. That might be a database job record, an outbox or a broker-backed workflow, depending on the design.
The distinction is not about whether the code uses an asynchronous keyword. It is about where unfinished work lives after the request and process that created it no longer exist.
Observe the Complete Journey
For direct calls, measure response duration, errors, timeouts and attempts per logical operation. A final success rate can look healthy while retries consume increasing time and capacity.
For queued work, measure acceptance rate, processing rate, oldest work age, retries and terminal failures. Also track the business result, such as accepted orders whose receipts remain unresolved beyond the promised interval.
Carry a stable operation identifier through logs and messages. A receipt may involve the original request, an outbox publisher, several delivery attempts and a provider callback. One identifier helps connect those pieces without depending on a single long-lived trace.
Assign someone responsibility for failed messages. A dead-letter queue is a place to inspect work that cannot continue automatically, not a place where obligations disappear. Operators need enough context to fix and safely redeliver the specific task.
Test recovery, not just throughput. Pause a worker, restore it and measure whether the backlog drains. Lose an HTTP response after a committed action and verify that the retry finds the original outcome. These experiments show whether the communication contract survives ordinary interruptions.
Choose a Pattern for Three Small Features
Consider a postcode lookup used while completing an address. The user needs a short list of addresses immediately, and the lookup does not create a durable business effect. A direct HTTP request with a short deadline is a reasonable starting point. If the service is unavailable, the interface can allow manual entry rather than blocking the whole purchase.
Now consider generating a year's worth of invoices as one download. The job can take minutes and requires a sizeable output file. A durable job record and queue fit the workflow better. The initial response returns the job identity, and the user can leave the page and retrieve the result later after a fresh access check.
Finally, consider checking whether a discount code is currently valid. The answer affects the order being placed. Sending a message and immediately assuming success would be incorrect. The service can perform a direct validation or move the whole order into an explicitly pending workflow, but it cannot quietly treat deferred work as a completed decision.
These examples show why latency tolerance alone is insufficient. We also need to know whether the answer changes a current business decision, whether the work has side effects and whether the caller can understand a pending outcome.
Make the Decision Reviewable
For each connection between services, record a short explanation of the chosen pattern. Include the caller's required result, the accepted waiting time, the owner of unfinished work and the recovery action after uncertainty.
For the receipt flow, that note might say: order acceptance records the receipt intent durably; workers may retry the same logical operation; the user can inspect receipt status; an unresolved provider result is investigated rather than blindly treated as failure.
For delivery quotes, the note might say: checkout waits within its allocated deadline; the response is a quote with a defined validity period; a failed call does not imply free delivery; retries are bounded and only used for outcomes the receiver permits.
Keep the note close to the code or workflow documentation so later changes can be assessed against it. Replacing a direct call with a message changes the timing contract. Adding a synchronous provider call to a worker's acknowledgement path changes failure and retry behaviour. Those changes deserve review even when the payload remains identical.
This small record is often more useful than declaring that the whole application is synchronous or event-driven. It explains what each connection is responsible for and gives a new engineer a concrete way to reason about a failure without first learning every component in the system.
Summary
Use a direct request when the caller needs an answer within the current interaction. Use queued work when the caller can accept a durable promise and the result can arrive later. Many useful systems combine both.
Define success at each boundary: database commit, broker acceptance, worker completion and external effect are different events. Safe retries, stable operation identifiers, durable task records and explicit progress make those distinctions manageable.
The best choice is the one whose timing and recovery behaviour matches the product. Follow one operation through a slow service, a lost response and a restart, and choose the pattern that can still explain what happened and what will happen next.
