
This post breaks down the difference between direct service calls, jobs and workers, and event-driven flows in plain terms, with a simple way to decide when each one makes sense.
title: Service-to-Service Calls vs Event-Driven Flows: When to Use Which published: true description: This post breaks down the difference between direct service calls, jobs and workers, and event-driven flows in plain terms, with a simple way to decide when each one makes sense. tags: Backend, Architecture
When a system starts getting bigger, one question shows up pretty quickly:
How should one service trigger work in another service?
In most teams, the answer usually falls into one of these two patterns:
Neither one is automatically better.
The real job is knowing when you need an immediate answer and when you do not.
There is also a third thing that often gets mixed into this conversation: jobs and workers.
They also handle work asynchronously, but they are not the same as events.
That sounds simple, but this is where a lot of systems get messy. Some teams push everything through synchronous calls. Others try to make everything event-driven because it feels more scalable or modern. And sometimes teams publish events when what they really needed was just a background job.
In practice, most healthy systems use all three where they fit.
This is the easier pattern to understand.
One service calls another service and waits for the result.
For example:
This is usually a request-response flow. One service asks a question or sends a command, and it needs the answer right away.
const paymentResult = await billingClient.charge({
customerId,
amountInCents: total,
});
if (!paymentResult.success) {
throw new Error("Payment failed");
}
That is a good fit when:
If payment fails, you probably should not confirm the order. That is why a direct call makes sense there.
Event-driven flows work differently.
Instead of one service directly telling another service what to do, a service publishes an event saying that something already happened.
Examples:
order.createdpayment.completeduser.signed_upOther parts of the system can listen and react to that event when they are ready.
await eventBus.publish({
type: "order.created",
payload: {
orderId,
customerId,
totalAmount,
},
});
After that event is published, other consumers might:
The important part is this: the original request does not need to sit there waiting for all of that work to finish.
That makes event-driven flows useful when:
This is the part that often gets blurred.
Jobs and workers are also about async work, but the idea is different.
With a job, the system is saying:
"This task needs to be done, just not inside the current request."
Examples:
A worker then pulls that job from a queue and processes it in the background.
This is a good fit when:
The short version is:
That difference matters.
If you publish an event just to get one background task done, you may be making the system more complicated than it needs to be.
Let us use a checkout flow because it is easy to picture.
When a customer places an order, several things may need to happen:
Should all of those happen through direct service calls?
Usually not.
Some of those steps decide whether the order can even go through. Others are just reactions to a successful order.
That distinction matters a lot.
Checking inventory and charging payment are usually part of the critical path. If either one fails, the order should not be confirmed.
Sending the confirmation email might be better as a background job handled by a worker.
Updating analytics might be better as a reaction to an event like order.confirmed.
Those things matter, but they usually do not need to block the checkout response.
That leads to a very practical question:
What must happen before we respond, and what can safely happen after?
Here is a practical way to think about it:
Another useful question is:
If the downstream step fails, should the original action fail too?
If the answer is yes, a direct call is often the right choice.
If the answer is no, then the next question is whether you need one background task or a broader event.
For example:
That one question will save you from a lot of over-engineering.
This usually starts with good intentions. Direct calls are easy to reason about, so teams keep adding one more call, then another, then another.
Before long, one user request depends on five other services being healthy at the same time.
That often leads to:
This goes wrong in the opposite direction.
Teams hear that event-driven systems scale well, so they start publishing events for almost every step, even simple ones.
Now a basic business flow is spread across queues, consumers, retries, dead-letter handling, and eventual consistency problems.
Sometimes that complexity is worth it. A lot of times, it is not.
Not every async task needs to become an event.
If the real need is simply "send this email in the background," a job queue and worker may be the cleaner solution.
Events are more useful when several parts of the system may need to respond independently.
An event should describe something meaningful that happened in the business.
user.registered is clear.
run-user-post-processing sounds more like a hidden remote procedure call.
Good events are usually named after facts, not internal tasks.
A lot of real systems end up with a hybrid approach.
For example:
order.confirmed.That gives you:
That is usually a much healthier setup than forcing everything into one model.
Service-to-service calls, jobs and workers, and event-driven flows are not competing ideas. They are tools for different situations.
Use direct calls when the current action needs an answer right now.
Use jobs when a specific task should happen in the background.
Use events when the main action can finish first and other systems can react afterward.
If you keep that one distinction clear, architecture decisions become a lot less confusing.
aiMost of us have seen a coding agent fail to complete a task we know it can do. We just don't...
googlecloudWhen building Generative AI applications, developers often encounter a massive bottleneck: sequential...
discussI’ve been thinking about sharing some electronic circuit posts on Dev.to — small circuits, DIY...
agentsWhat nobody tells you about exporting your multi-agent prototype to a local workspace. Every...
agenticarchitectAutonomous agents are genuinely good at answering messy business questions. Give one an LLM and a set...
aiPR volume went up, ticket quality didn't, and the gap got filled with LLMs on both sides of the review: bots reviewing, bots replying, bots occasionally arguing with bots about priorities that only existed in a teammate's head. Our CEO named the actual problem, and it's bigger than code review.
Workflows from the Neura Market marketplace related to this Stable Diffusion resource