<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rahmon Toyeeb</title>
    <description>The latest articles on DEV Community by Rahmon Toyeeb (@toybz).</description>
    <link>https://dev.to/toybz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F117864%2F9499d7f7-3057-4680-8b1a-1a206d13f2b1.jpeg</url>
      <title>DEV Community: Rahmon Toyeeb</title>
      <link>https://dev.to/toybz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toybz"/>
    <language>en</language>
    <item>
      <title>Service-to-Service Calls vs Event-Driven Flows: When to Use Which</title>
      <dc:creator>Rahmon Toyeeb</dc:creator>
      <pubDate>Sun, 26 Apr 2026 13:10:27 +0000</pubDate>
      <link>https://dev.to/toybz/service-to-service-calls-vs-event-driven-flows-when-to-use-which-1da8</link>
      <guid>https://dev.to/toybz/service-to-service-calls-vs-event-driven-flows-when-to-use-which-1da8</guid>
      <description>&lt;h2&gt;
  
  
  Two common ways services talk to each other
&lt;/h2&gt;

&lt;p&gt;When a system starts getting bigger, one question shows up pretty quickly:&lt;/p&gt;

&lt;p&gt;How should one service trigger work in another service?&lt;/p&gt;

&lt;p&gt;In most teams, the answer usually falls into one of these two patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;direct service-to-service calls&lt;/li&gt;
&lt;li&gt;event-driven flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither one is automatically better.&lt;/p&gt;

&lt;p&gt;The real job is knowing when you need an immediate answer and when you do not.&lt;/p&gt;

&lt;p&gt;There is also a third thing that often gets mixed into this conversation: jobs and workers.&lt;/p&gt;

&lt;p&gt;They also handle work asynchronously, but they are not the same as events.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;In practice, most healthy systems use all three where they fit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Direct service-to-service calls
&lt;/h2&gt;

&lt;p&gt;This is the easier pattern to understand.&lt;/p&gt;

&lt;p&gt;One service calls another service and waits for the result.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the checkout service calls billing to charge a card&lt;/li&gt;
&lt;li&gt;the order service calls inventory to reserve stock&lt;/li&gt;
&lt;li&gt;an admin API calls the permissions service before allowing an action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is usually a request-response flow. One service asks a question or sends a command, and it needs the answer right away.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;billingClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amountInCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;paymentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a good fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the user cannot move forward without the result&lt;/li&gt;
&lt;li&gt;the current request should fail if the downstream step fails&lt;/li&gt;
&lt;li&gt;the steps need to happen in order (synchronous)&lt;/li&gt;
&lt;li&gt;you want a clear yes-or-no answer immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If payment fails, you probably should not confirm the order. That is why a direct call makes sense there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event-driven flows
&lt;/h2&gt;

&lt;p&gt;Event-driven flows work differently.&lt;/p&gt;

&lt;p&gt;Instead of one service directly telling another service what to do, a service publishes an event saying that something already happened.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;order.created&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;payment.completed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user.signed_up&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other parts of the system can listen and react to that event when they are ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;totalAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that event is published, other consumers might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send a confirmation email&lt;/li&gt;
&lt;li&gt;update analytics&lt;/li&gt;
&lt;li&gt;notify a shipping system&lt;/li&gt;
&lt;li&gt;notify a loyalty system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is this: the original request does not need to sit there waiting for all of that work to finish.&lt;/p&gt;

&lt;p&gt;That makes event-driven flows useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the follow-up work can happen later&lt;/li&gt;
&lt;li&gt;multiple systems need to react to the same event&lt;/li&gt;
&lt;li&gt;you want services to be less tightly coupled&lt;/li&gt;
&lt;li&gt;eventual consistency is acceptable&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Jobs and workers are different from events
&lt;/h2&gt;

&lt;p&gt;This is the part that often gets blurred.&lt;/p&gt;

&lt;p&gt;Jobs and workers are also about async work, but the idea is different.&lt;/p&gt;

&lt;p&gt;With a job, the system is saying:&lt;/p&gt;

&lt;p&gt;"This task needs to be done, just not inside the current request."&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send this email&lt;/li&gt;
&lt;li&gt;resize this uploaded image&lt;/li&gt;
&lt;li&gt;generate this invoice PDF&lt;/li&gt;
&lt;li&gt;retry this failed webhook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A worker then pulls that job from a queue and processes it in the background.&lt;/p&gt;

&lt;p&gt;This is a good fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one system already owns the task&lt;/li&gt;
&lt;li&gt;the work can happen later&lt;/li&gt;
&lt;li&gt;you want retries and queue-based processing&lt;/li&gt;
&lt;li&gt;you do not need multiple systems reacting to the same fact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The short version is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a job means "do this task later"&lt;/li&gt;
&lt;li&gt;an event means "this thing happened"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple checkout example
&lt;/h2&gt;

&lt;p&gt;Let us use a checkout flow because it is easy to picture.&lt;/p&gt;

&lt;p&gt;When a customer places an order, several things may need to happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;check inventory&lt;/li&gt;
&lt;li&gt;charge payment&lt;/li&gt;
&lt;li&gt;send a confirmation email&lt;/li&gt;
&lt;li&gt;update analytics&lt;/li&gt;
&lt;li&gt;prepare shipping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Should all of those happen through direct service calls?&lt;/p&gt;

&lt;p&gt;Usually not.&lt;/p&gt;

&lt;p&gt;Some of those steps decide whether the order can even go through. Others are just reactions to a successful order.&lt;/p&gt;

&lt;p&gt;That distinction matters a lot.&lt;/p&gt;

&lt;p&gt;Checking inventory and charging payment are usually part of the critical path. If either one fails, the order should not be confirmed.&lt;/p&gt;

&lt;p&gt;Sending the confirmation email might be better as a background job handled by a worker.&lt;/p&gt;

&lt;p&gt;Updating analytics might be better as a reaction to an event like &lt;code&gt;order.confirmed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those things matter, but they usually do not need to block the checkout response.&lt;/p&gt;

&lt;p&gt;That leads to a very practical question:&lt;/p&gt;

&lt;p&gt;What must happen before we respond, and what can safely happen after?&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple rule that helps
&lt;/h2&gt;

&lt;p&gt;Here is a practical way to think about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if the current action depends on the answer right now, use a direct call&lt;/li&gt;
&lt;li&gt;if one known task should happen but not block the request chain, use a (background) job and worker&lt;/li&gt;
&lt;li&gt;if multiple systems may react to something that happened, use an event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another useful question is:&lt;/p&gt;

&lt;p&gt;If the downstream step fails, should the original action fail too?&lt;/p&gt;

&lt;p&gt;If the answer is yes, a direct call is often the right choice.&lt;/p&gt;

&lt;p&gt;If the answer is no, then the next question is whether you need one background task or a broader event.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if fraud checking fails and you cannot approve the payment without it, use a direct call&lt;/li&gt;
&lt;li&gt;if a receipt email should be sent later by one worker, use a job&lt;/li&gt;
&lt;li&gt;if analytics, loyalty, and notifications may all react to a confirmed order, publish an event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one question will save you from a lot of over-engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where teams get this wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Making everything synchronous
&lt;/h3&gt;

&lt;p&gt;This usually starts with good intentions. Direct calls are easy to reason about, so teams keep adding one more call, then another, then another.&lt;/p&gt;

&lt;p&gt;Before long, one user request depends on five other services being healthy at the same time.&lt;/p&gt;

&lt;p&gt;That often leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slower APIs&lt;/li&gt;
&lt;li&gt;fragile request chains&lt;/li&gt;
&lt;li&gt;harder incident recovery&lt;/li&gt;
&lt;li&gt;failures spreading across services&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Making everything event-driven
&lt;/h3&gt;

&lt;p&gt;This goes wrong in the opposite direction.&lt;/p&gt;

&lt;p&gt;Teams hear that event-driven systems scale well, so they start publishing events for almost every step, even simple ones.&lt;/p&gt;

&lt;p&gt;Now a basic business flow is spread across queues, consumers, retries, dead-letter handling, and eventual consistency problems.&lt;/p&gt;

&lt;p&gt;Sometimes that complexity is worth it. A lot of times, it is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using events for work that is really just a job
&lt;/h3&gt;

&lt;p&gt;Not every async task needs to become an event.&lt;/p&gt;

&lt;p&gt;If the real need is simply "send this email in the background," a job queue and worker may be the cleaner solution.&lt;/p&gt;

&lt;p&gt;Events are more useful when several parts of the system may need to respond independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Publishing vague events
&lt;/h3&gt;

&lt;p&gt;An event should describe something meaningful that happened in the business.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;user.registered&lt;/code&gt; is clear.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;run-user-post-processing&lt;/code&gt; sounds more like a hidden remote procedure call.&lt;/p&gt;

&lt;p&gt;Good events are usually named after facts, not internal tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  A balanced way to design it
&lt;/h2&gt;

&lt;p&gt;A lot of real systems end up with a hybrid approach.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checkout directly calls inventory.&lt;/li&gt;
&lt;li&gt;Checkout directly calls billing.&lt;/li&gt;
&lt;li&gt;If both succeed, the order is confirmed.&lt;/li&gt;
&lt;li&gt;A job is queued to send the confirmation email.&lt;/li&gt;
&lt;li&gt;The system publishes &lt;code&gt;order.confirmed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Analytics, loyalty, and other consumers react asynchronously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;immediate answers for the steps that actually decide success&lt;/li&gt;
&lt;li&gt;background processing for tasks with clear ownership&lt;/li&gt;
&lt;li&gt;faster user-facing responses for the steps that can wait&lt;/li&gt;
&lt;li&gt;clearer boundaries between critical work and side effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is usually a much healthier setup than forcing everything into one model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Service-to-service calls, jobs and workers, and event-driven flows are not competing ideas. They are tools for different situations.&lt;/p&gt;

&lt;p&gt;Use direct calls when the current action needs an answer right now.&lt;/p&gt;

&lt;p&gt;Use jobs when a specific task should happen  in the background.&lt;/p&gt;

&lt;p&gt;Use events when the main action can finish first and other systems can react afterward.&lt;/p&gt;

&lt;p&gt;If you keep that one distinction clear, architecture decisions become a lot less confusing.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
