Once a call has to leave the process, how it leaves becomes a decision you actually have to make, instead of something the runtime just handles for you:
| Synchronous (request/response) | Asynchronous (messaging) | |
|---|---|---|
| Example | HTTP request, gRPC call | Message queue, event stream |
| Caller waits for an answer | Yes | No — publishes and moves on |
| Coupling | The callee needs to be reachable right now | Only the broker needs to be up; a slow consumer just falls behind and works through the backlog whenever it resumes |
| Failure mode | The caller stalls, times out, or gets an error back immediately | Messages pile up in the queue instead of failing the caller outright |
| Good fit for | A question the next step genuinely can't proceed without answering | A fact worth broadcasting, that whoever's interested can pick up whenever suits them |
Furrow's checkout flow needs a synchronous answer to one specific question: is there still enough lettuce in zone 3's harvest lot to cover this box, right now, before the confirmation screen can show up. That has to be a request/response call to Harvest, because there is genuinely nothing useful to do with "someone will let you know eventually" at that moment — the subscriber is staring at a loading spinner.
But "the box is confirmed" is a completely different kind of fact. Once it's true, a pile of things need to react to it — generate the packing-list line item, calculate the farmer's payout for that lot, queue the "your box is on its way" text — and not one of them needs to hold up the checkout screen while it happens. Furrow's original synchronous version chained all of that inside the same request that charged the card, and a slow SMS provider on a Tuesday afternoon turned into a six-second hang on every single checkout, for a step that had nothing to do with whether the box was actually confirmed. Moving "box confirmed" to a published event that the packing-list, payout, and notification logic each subscribe to independently fixed exactly that — checkout latency dropped to whatever the card charge itself takes, because nothing downstream is in the critical path anymore.
The trade going the other way is real too: async buys resilience by giving up an immediate answer. A "harvest running low" notification to the Farm Partnerships team doesn't need to be synchronous, and making it so would just mean an unrelated notification-service hiccup could somehow block a farmer from updating their inventory count. Most working systems, Furrow included, end up using both — synchronous calls for the small number of things that genuinely need an answer before the next step can happen, async events for everything that's really just "this occurred, react on your own time."