beginner 2 min answer Multiple choice

A user signup request takes 4 seconds at p99. Tracing shows the handler sends a welcome email through a third-party API and writes to a CRM before returning. What is wrong architecturally?

separation-of-concernsasyncavailabilitycouplinggithub
Pick one
Show the full answer Hide the answer

What is being tested

The most common and most consequential separation-of-concerns violation: a synchronous side effect that has no business being synchronous.

The reasoning

Ask what the user is actually waiting for. They want to know their account exists and they are logged in. They are not waiting for a CRM record, and they are certainly not waiting for an email that will arrive in their inbox whenever it arrives.

By putting both in the request path you have made two decisions you probably did not intend:

Your availability is now the product of three systems. If the email provider has a bad ten minutes, signups fail. Your service is healthy; your users cannot sign up. Formally, you have imported an external dependency's availability into a flow that does not need it.

Your latency is now the sum of three systems' tails. Even when everything works, p99 is dominated by whichever third party is having the worst second.

There is a third, subtler problem: what happens if the email succeeds and the CRM write fails? You now either return an error to a user whose account was in fact created — who will retry and receive "email already registered" — or you swallow the error and lose the CRM record silently. Neither is good, and the situation only exists because unrelated concerns were made atomic.

The fix

Commit the user record, then publish an event. Consumers handle email and CRM independently, with retries, backoff and a dead-letter queue for what cannot be delivered.

The one piece of rigour that matters: if you write the user row and publish the event as two separate operations, a crash between them loses the event. Use the transactional outbox pattern — write the event into the same database transaction as the user row, and let a relay publish it — or accept the gap knowingly and build a reconciliation job.

Industry parallel

GitHub separates this cleanly: a git push is fast because everything triggered by it — webhooks, CI runs, notifications, search indexing — happens asynchronously afterwards. If indexing ran inline, one pathological repository would slow pushes for everyone. The product consequence is that search can be seconds stale, which is exactly the right thing to sacrifice.

What to check before moving everything to async

Some side effects genuinely belong in the request path: anything the user's next action depends on, anything that must be atomic with the primary write, and anything where a delayed failure cannot be recovered. Payment authorisation before showing an order confirmation is synchronous for good reason. The test is whether a delay is visible and harmful, not whether async is fashionable.