<?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>Forem: Sheikh Shahzaman</title>
    <description>The latest articles on Forem by Sheikh Shahzaman (@shahzamandev).</description>
    <link>https://forem.com/shahzamandev</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%2F3848452%2F42ec2fce-4b9c-4483-97a6-a1ffda222802.jpg</url>
      <title>Forem: Sheikh Shahzaman</title>
      <link>https://forem.com/shahzamandev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/shahzamandev"/>
    <language>en</language>
    <item>
      <title>How Architecture Tests Caught a Data Isolation Bug Before It Reached Production</title>
      <dc:creator>Sheikh Shahzaman</dc:creator>
      <pubDate>Sat, 25 Apr 2026 09:52:39 +0000</pubDate>
      <link>https://forem.com/shahzamandev/how-architecture-tests-caught-a-data-isolation-bug-before-it-reached-production-3en4</link>
      <guid>https://forem.com/shahzamandev/how-architecture-tests-caught-a-data-isolation-bug-before-it-reached-production-3en4</guid>
      <description>&lt;p&gt;TL;DR: Pest PHP architecture tests let you write your team coding rules as automated tests. Controllers using the DB facade directly? Build fails. Model missing the tenant isolation trait? Build fails. These tests caught a critical data isolation bug three weeks after we added them.&lt;/p&gt;




&lt;p&gt;We had a rule in our team wiki. Every model that stores tenant-specific data must use the BelongsToTenant trait. This trait adds a global scope that automatically filters all queries by the current tenant. Without it a query could accidentally return data from other tenants.&lt;/p&gt;

&lt;p&gt;The rule was clear. Everyone knew about it. It was mentioned in onboarding. It was in our code review checklist.&lt;/p&gt;

&lt;p&gt;A new developer joined the team. Three weeks later they added a new model without the trait. The code review missed it because the reviewer was focused on the business logic which was actually well written. The model went to production.&lt;/p&gt;

&lt;p&gt;For two days one tenant could see fragments of another tenant data in a specific report. We caught it from a support ticket not from our tests.&lt;/p&gt;

&lt;p&gt;That was the day I added architecture tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Architecture Tests Look Like
&lt;/h2&gt;

&lt;p&gt;Pest PHP includes an arch() function that lets you write assertions about your code structure. Not about what your code does but about how it is organized.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/Architecture/ArchTest.php&lt;/span&gt;

&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'models must use BelongsToTenant trait'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Models'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUseTrait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Traits\BelongsToTenant'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'controllers should not use DB facade'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Http\Controllers'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Illuminate\Support\Facades\DB'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services should not use Request'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Services'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Illuminate\Http\Request'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'value objects must be readonly'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\ValueObjects'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBeReadonly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'soft delete models must also have tenant scope'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Models'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUseTrait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Illuminate\Database\Eloquent\SoftDeletes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUseTrait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Traits\BelongsToTenant'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tests run in our CI pipeline on every commit. If any rule is violated the build fails. The developer gets a clear error message telling them exactly which rule they broke and which file broke it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tests That Have Saved Us
&lt;/h2&gt;

&lt;p&gt;Here are the architecture tests that have caught the most violations in the last 6 months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tenant isolation test&lt;/strong&gt; caught 4 new models that were missing the BelongsToTenant trait. Each one would have been a potential data leak. This single test justified the entire effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The controller purity test&lt;/strong&gt; catches developers who write database queries directly in controllers instead of using service classes. This happens more often than you would think especially when someone is "just adding a quick endpoint."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This would fail the architecture test&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReportController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&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;The right approach is moving that query into a service class. The controller just calls the service and returns the result. This keeps controllers thin and makes the business logic testable in isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dependency direction test&lt;/strong&gt; ensures that inner layers never depend on outer layers. Services do not import Request objects. Models do not import controller classes. This keeps the dependency graph clean and makes the code easier to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Beyond Basic Rules
&lt;/h2&gt;

&lt;p&gt;The basic trait and dependency checks are useful but the real power comes from combining architecture tests with custom logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'controller methods should use Form Requests'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Http\Controllers'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Illuminate\Http\Request'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'no env calls outside config'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUse&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'env'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The env() test is one I wish I had added earlier. Calling env() directly in application code works in development but returns null when the config is cached in production. This is a classic Laravel gotcha that catches developers at least once. The architecture test prevents it permanently.&lt;/p&gt;

&lt;p&gt;I also added a test that ensures no model uses the $guarded property set to an empty array. We had an incident where a mass assignment vulnerability slipped through because a developer set $guarded to an empty array thinking it was the same as setting $fillable. It is not. An empty $guarded means every field is mass assignable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'models should not use empty guarded'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Models'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUseStrictlyEmptyGuarded&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Mistake I Made Along the Way
&lt;/h2&gt;

&lt;p&gt;When I first set up architecture tests I went overboard. I added 20 rules in one pull request. The CI pipeline lit up like a Christmas tree. Over 150 violations across the codebase.&lt;/p&gt;

&lt;p&gt;The team was annoyed. Nobody wanted to fix 150 violations that had nothing to do with their current feature work. I had to revert the PR and take a different approach.&lt;/p&gt;

&lt;p&gt;The lesson was clear. You can not enforce architectural rules retroactively on a large codebase all at once. It does not work socially even if it works technically.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Introduce Architecture Tests to an Existing Project
&lt;/h2&gt;

&lt;p&gt;Here is the approach that worked for us after I learned from that mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1.&lt;/strong&gt; Start with the most critical rule only. For us that was the tenant isolation trait. One test. Fix the handful of violations. Merge it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2.&lt;/strong&gt; Add one new rule per week. Give the team time to adjust and fix existing violations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3.&lt;/strong&gt; For rules with many existing violations use the ignoring() method to exclude legacy code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'controllers should not use DB'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Http\Controllers'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Illuminate\Support\Facades\DB'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ignoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'App\Http\Controllers\Legacy'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you enforce the rule for all new code while giving the team time to refactor old code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4.&lt;/strong&gt; Track violations over time. We added a simple script that counts the number of ignoring() exceptions. That number should go down every sprint not up. We went from 47 exceptions down to 12 over four months. The trend line matters more than the absolute number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Numbers from Production
&lt;/h2&gt;

&lt;p&gt;After running architecture tests for 6 months the results spoke for themselves.&lt;/p&gt;

&lt;p&gt;The tenant isolation test caught 4 models missing the BelongsToTenant trait. Each would have been a data leak between tenants. Response time to catch these dropped from "whenever someone notices in production" to "immediately in CI."&lt;/p&gt;

&lt;p&gt;The controller purity test caught 23 violations in the first month alone. After 3 months that number dropped to 2 per month. Developers learned the pattern and stopped writing database queries in controllers.&lt;/p&gt;

&lt;p&gt;The env() test caught 7 direct calls that would have broken in production when config caching was enabled. One of those was in the payment processing flow. That single catch probably saved us a weekend of debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;I would add architecture tests on day one of every new project. Adding them to an existing codebase is painful because of all the existing violations. Starting fresh means the rules are enforced from the first commit and there is never any catch-up work.&lt;/p&gt;

&lt;p&gt;I also would have been more aggressive about testing dependency directions. We caught the DB-in-controller pattern early but we were slow to add tests for other dependency violations. By the time we added them there were 40+ violations to fix.&lt;/p&gt;

&lt;p&gt;And I would create a shared package of common architecture tests that every project uses. We ended up writing very similar tests across three different projects. A reusable test suite would have saved time. I have not built that package yet but it is on my list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;Architecture tests are not about being strict for the sake of being strict. They are about making your codebase predictable.&lt;/p&gt;

&lt;p&gt;When every model has the tenant trait you know data isolation is enforced everywhere. When no controller touches the database directly you know the service layer is the single source of truth. When dependencies flow in one direction you know refactoring one layer will not break another.&lt;/p&gt;

&lt;p&gt;These guarantees compound over time. Six months in the codebase is still clean because the tests prevent it from degrading. New developers write code that follows the patterns automatically because the CI pipeline will not let them do otherwise.&lt;/p&gt;

&lt;p&gt;Your team rules should live in your test suite not in a wiki.&lt;/p&gt;

&lt;p&gt;What architecture rules would you enforce if you had automated tests for them?&lt;/p&gt;

</description>
      <category>testing</category>
      <category>laravel</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why We Switched from Direct API Calls to Kafka and What Broke Along the Way</title>
      <dc:creator>Sheikh Shahzaman</dc:creator>
      <pubDate>Sat, 25 Apr 2026 09:38:22 +0000</pubDate>
      <link>https://forem.com/shahzamandev/why-we-switched-from-direct-api-calls-to-kafka-and-what-broke-along-the-way-4ag5</link>
      <guid>https://forem.com/shahzamandev/why-we-switched-from-direct-api-calls-to-kafka-and-what-broke-along-the-way-4ag5</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We migrated 10+ microservices from direct HTTP calls to Kafka event-driven communication. Reliability improved massively but the migration was harder than expected. Here are the real lessons including the mistakes.&lt;/p&gt;




&lt;p&gt;Our system started as a monolith. Then we split it into microservices. The services talked to each other using direct HTTP calls. Service A would POST to Service B which would POST to Service C. It worked fine when we had 3 services.&lt;/p&gt;

&lt;p&gt;Then we had 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Day Everything Cascaded
&lt;/h2&gt;

&lt;p&gt;One Tuesday morning our notification service crashed because of a memory leak. No big deal right? Restart it and move on.&lt;/p&gt;

&lt;p&gt;But the order service was calling the notification service directly during checkout. When notification service was down the order endpoint started timing out. Users could not place orders. The billing service was also calling notification service to confirm payment receipts. Billing started failing too.&lt;/p&gt;

&lt;p&gt;One crashed service took down three other services because they were all directly dependent on it.&lt;/p&gt;

&lt;p&gt;That was the day we decided to move to event-driven architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Set It Up
&lt;/h2&gt;

&lt;p&gt;The concept is simple. Instead of Service A calling Service B directly Service A publishes an event to Kafka. Service B listens for that event and processes it on its own time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: Direct coupling&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'billing-service/invoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'notification-service/email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'analytics-service/track'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&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;span class="c1"&gt;// After: Event-driven&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;KafkaProducer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order.completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'order_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'tenant_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'completed_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toIso8601String&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order service does not know or care who listens to that event. Billing creates an invoice. Notifications send an email. Analytics tracks a metric. Each service subscribes to the event independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke During Migration
&lt;/h2&gt;

&lt;p&gt;I wish I could say the migration was smooth. It was not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: Event ordering.&lt;/strong&gt; We assumed events would arrive in the order they were published. They mostly did. But when we had high throughput some consumers processed events out of order. An "order.updated" event arrived before "order.created" and the consumer crashed because the order did not exist yet.&lt;/p&gt;

&lt;p&gt;The fix was adding an event version number and having consumers check if they had already processed a newer version before applying changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: Duplicate events.&lt;/strong&gt; Kafka guarantees at-least-once delivery. That means consumers can receive the same event twice. We had a bug where a payment was processed twice because the consumer was not idempotent.&lt;/p&gt;

&lt;p&gt;The fix was adding a unique event ID and checking if we had already processed that ID before taking action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceConsumer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&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="nc"&gt;ProcessedEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createFromOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order_id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nc"&gt;ProcessedEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&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;&lt;strong&gt;Problem 3: Debugging was harder.&lt;/strong&gt; With direct API calls you could trace a request from start to finish in one log. With events the flow is split across multiple services and multiple time periods. Finding out why an invoice was not created required checking logs in three different services.&lt;/p&gt;

&lt;p&gt;We solved this by adding a correlation ID to every event. When the order service publishes an event it includes a unique request ID. Every downstream consumer includes that same ID in their logs. Now you can search for one ID and see the entire flow across all services.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Patterns That Saved Us
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dead letter queue.&lt;/strong&gt; When a consumer fails to process an event after 3 retries it goes to a dead letter topic. We have a dashboard that shows failed events and lets us replay them after fixing the bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema registry.&lt;/strong&gt; We define the structure of every event in a shared schema. If a producer tries to publish an event that does not match the schema it fails at publish time not at consume time. This prevented so many bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer lag monitoring.&lt;/strong&gt; We track how far behind each consumer is. If the notification consumer falls 10,000 events behind we get an alert. This caught performance issues before users noticed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;After 3 months on event-driven architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero cascading failures. One service going down does not affect any other service.&lt;/li&gt;
&lt;li&gt;We can deploy services independently without coordinating with other teams.&lt;/li&gt;
&lt;li&gt;Adding a new consumer takes 30 minutes instead of modifying 5 different services.&lt;/li&gt;
&lt;li&gt;Event replay lets us reprocess historical data when we add new features.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;I would have implemented idempotency from day one not after the duplicate payment bug. Every consumer should be idempotent by default.&lt;/p&gt;

&lt;p&gt;I would have invested in better tooling earlier. A good event viewer that shows the flow of events across services would have saved weeks of debugging time.&lt;/p&gt;

&lt;p&gt;And I would not have migrated everything at once. We tried to move all 10 services in one sprint. It should have been gradual. Start with the least critical services and work toward the most critical.&lt;/p&gt;

&lt;p&gt;Event-driven architecture is powerful but it adds complexity. If you have 3 services that rarely fail direct API calls are probably fine. If you have 10+ services and reliability matters events are worth the investment.&lt;/p&gt;

&lt;p&gt;Have you migrated from direct API calls to event-driven architecture? What surprised you the most?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why We Switched from Direct API Calls to Kafka and What Broke Along the Way</title>
      <dc:creator>Sheikh Shahzaman</dc:creator>
      <pubDate>Tue, 21 Apr 2026 05:21:38 +0000</pubDate>
      <link>https://forem.com/shahzamandev/why-we-switched-from-direct-api-calls-to-kafka-and-what-broke-along-the-way-42me</link>
      <guid>https://forem.com/shahzamandev/why-we-switched-from-direct-api-calls-to-kafka-and-what-broke-along-the-way-42me</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We migrated 10+ microservices from direct HTTP calls to Kafka event-driven communication. Reliability improved massively but the migration was harder than expected. Here are the real lessons including the mistakes.&lt;/p&gt;




&lt;p&gt;Our system started as a monolith. Then we split it into microservices. The services talked to each other using direct HTTP calls. Service A would POST to Service B which would POST to Service C. It worked fine when we had 3 services.&lt;/p&gt;

&lt;p&gt;Then we had 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Day Everything Cascaded
&lt;/h2&gt;

&lt;p&gt;One Tuesday morning our notification service crashed because of a memory leak. No big deal right? Restart it and move on.&lt;/p&gt;

&lt;p&gt;But the order service was calling the notification service directly during checkout. When notification service was down the order endpoint started timing out. Users could not place orders. The billing service was also calling notification service to confirm payment receipts. Billing started failing too.&lt;/p&gt;

&lt;p&gt;One crashed service took down three other services because they were all directly dependent on it.&lt;/p&gt;

&lt;p&gt;That was the day we decided to move to event-driven architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Set It Up
&lt;/h2&gt;

&lt;p&gt;The concept is simple. Instead of Service A calling Service B directly Service A publishes an event to Kafka. Service B listens for that event and processes it on its own time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: Direct coupling&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'billing-service/invoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'notification-service/email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'analytics-service/track'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&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;span class="c1"&gt;// After: Event-driven&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;KafkaProducer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order.completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'order_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'tenant_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'completed_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toIso8601String&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order service does not know or care who listens to that event. Billing creates an invoice. Notifications send an email. Analytics tracks a metric. Each service subscribes to the event independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke During Migration
&lt;/h2&gt;

&lt;p&gt;I wish I could say the migration was smooth. It was not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: Event ordering.&lt;/strong&gt; We assumed events would arrive in the order they were published. They mostly did. But when we had high throughput some consumers processed events out of order. An "order.updated" event arrived before "order.created" and the consumer crashed because the order did not exist yet.&lt;/p&gt;

&lt;p&gt;The fix was adding an event version number and having consumers check if they had already processed a newer version before applying changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: Duplicate events.&lt;/strong&gt; Kafka guarantees at-least-once delivery. That means consumers can receive the same event twice. We had a bug where a payment was processed twice because the consumer was not idempotent.&lt;/p&gt;

&lt;p&gt;The fix was adding a unique event ID and checking if we had already processed that ID before taking action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceConsumer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&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="nc"&gt;ProcessedEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createFromOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order_id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nc"&gt;ProcessedEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&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;&lt;strong&gt;Problem 3: Debugging was harder.&lt;/strong&gt; With direct API calls you could trace a request from start to finish in one log. With events the flow is split across multiple services and multiple time periods. Finding out why an invoice was not created required checking logs in three different services.&lt;/p&gt;

&lt;p&gt;We solved this by adding a correlation ID to every event. When the order service publishes an event it includes a unique request ID. Every downstream consumer includes that same ID in their logs. Now you can search for one ID and see the entire flow across all services.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Patterns That Saved Us
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dead letter queue.&lt;/strong&gt; When a consumer fails to process an event after 3 retries it goes to a dead letter topic. We have a dashboard that shows failed events and lets us replay them after fixing the bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema registry.&lt;/strong&gt; We define the structure of every event in a shared schema. If a producer tries to publish an event that does not match the schema it fails at publish time not at consume time. This prevented so many bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer lag monitoring.&lt;/strong&gt; We track how far behind each consumer is. If the notification consumer falls 10,000 events behind we get an alert. This caught performance issues before users noticed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;After 3 months on event-driven architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero cascading failures. One service going down does not affect any other service.&lt;/li&gt;
&lt;li&gt;We can deploy services independently without coordinating with other teams.&lt;/li&gt;
&lt;li&gt;Adding a new consumer takes 30 minutes instead of modifying 5 different services.&lt;/li&gt;
&lt;li&gt;Event replay lets us reprocess historical data when we add new features.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;I would have implemented idempotency from day one not after the duplicate payment bug. Every consumer should be idempotent by default.&lt;/p&gt;

&lt;p&gt;I would have invested in better tooling earlier. A good event viewer that shows the flow of events across services would have saved weeks of debugging time.&lt;/p&gt;

&lt;p&gt;And I would not have migrated everything at once. We tried to move all 10 services in one sprint. It should have been gradual. Start with the least critical services and work toward the most critical.&lt;/p&gt;

&lt;p&gt;Event-driven architecture is powerful but it adds complexity. If you have 3 services that rarely fail direct API calls are probably fine. If you have 10+ services and reliability matters events are worth the investment.&lt;/p&gt;

&lt;p&gt;Have you migrated from direct API calls to event-driven architecture? What surprised you the most?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why We Switched from Direct API Calls to Kafka and What Broke Along the Way</title>
      <dc:creator>Sheikh Shahzaman</dc:creator>
      <pubDate>Wed, 15 Apr 2026 01:45:38 +0000</pubDate>
      <link>https://forem.com/shahzamandev/why-we-switched-from-direct-api-calls-to-kafka-and-what-broke-along-the-way-24a2</link>
      <guid>https://forem.com/shahzamandev/why-we-switched-from-direct-api-calls-to-kafka-and-what-broke-along-the-way-24a2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgam4zv9hk2ors90l0v3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgam4zv9hk2ors90l0v3.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We migrated 10+ microservices from direct HTTP calls to Kafka event-driven communication. Reliability improved massively but the migration was harder than expected. Here are the real lessons including the mistakes.&lt;/p&gt;




&lt;p&gt;Our system started as a monolith. Then we split it into microservices. The services talked to each other using direct HTTP calls. Service A would POST to Service B which would POST to Service C. It worked fine when we had 3 services.&lt;/p&gt;

&lt;p&gt;Then we had 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Day Everything Cascaded
&lt;/h2&gt;

&lt;p&gt;One Tuesday morning our notification service crashed because of a memory leak. No big deal right? Restart it and move on.&lt;/p&gt;

&lt;p&gt;But the order service was calling the notification service directly during checkout. When notification service was down the order endpoint started timing out. Users could not place orders. The billing service was also calling notification service to confirm payment receipts. Billing started failing too.&lt;/p&gt;

&lt;p&gt;One crashed service took down three other services because they were all directly dependent on it.&lt;/p&gt;

&lt;p&gt;That was the day we decided to move to event-driven architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Set It Up
&lt;/h2&gt;

&lt;p&gt;The concept is simple. Instead of Service A calling Service B directly Service A publishes an event to Kafka. Service B listens for that event and processes it on its own time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: Direct coupling&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'billing-service/invoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'notification-service/email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'analytics-service/track'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&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;span class="c1"&gt;// After: Event-driven&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;markComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;KafkaProducer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order.completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'order_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'tenant_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'completed_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toIso8601String&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order service does not know or care who listens to that event. Billing creates an invoice. Notifications send an email. Analytics tracks a metric. Each service subscribes to the event independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke During Migration
&lt;/h2&gt;

&lt;p&gt;I wish I could say the migration was smooth. It was not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: Event ordering.&lt;/strong&gt; We assumed events would arrive in the order they were published. They mostly did. But when we had high throughput some consumers processed events out of order. An "order.updated" event arrived before "order.created" and the consumer crashed because the order did not exist yet.&lt;/p&gt;

&lt;p&gt;The fix was adding an event version number and having consumers check if they had already processed a newer version before applying changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: Duplicate events.&lt;/strong&gt; Kafka guarantees at-least-once delivery. That means consumers can receive the same event twice. We had a bug where a payment was processed twice because the consumer was not idempotent.&lt;/p&gt;

&lt;p&gt;The fix was adding a unique event ID and checking if we had already processed that ID before taking action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceConsumer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&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="nc"&gt;ProcessedEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createFromOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order_id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nc"&gt;ProcessedEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&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;&lt;strong&gt;Problem 3: Debugging was harder.&lt;/strong&gt; With direct API calls you could trace a request from start to finish in one log. With events the flow is split across multiple services and multiple time periods. Finding out why an invoice was not created required checking logs in three different services.&lt;/p&gt;

&lt;p&gt;We solved this by adding a correlation ID to every event. When the order service publishes an event it includes a unique request ID. Every downstream consumer includes that same ID in their logs. Now you can search for one ID and see the entire flow across all services.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Patterns That Saved Us
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dead letter queue.&lt;/strong&gt; When a consumer fails to process an event after 3 retries it goes to a dead letter topic. We have a dashboard that shows failed events and lets us replay them after fixing the bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema registry.&lt;/strong&gt; We define the structure of every event in a shared schema. If a producer tries to publish an event that does not match the schema it fails at publish time not at consume time. This prevented so many bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer lag monitoring.&lt;/strong&gt; We track how far behind each consumer is. If the notification consumer falls 10,000 events behind we get an alert. This caught performance issues before users noticed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;After 3 months on event-driven architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero cascading failures. One service going down does not affect any other service.&lt;/li&gt;
&lt;li&gt;We can deploy services independently without coordinating with other teams.&lt;/li&gt;
&lt;li&gt;Adding a new consumer takes 30 minutes instead of modifying 5 different services.&lt;/li&gt;
&lt;li&gt;Event replay lets us reprocess historical data when we add new features.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;I would have implemented idempotency from day one not after the duplicate payment bug. Every consumer should be idempotent by default.&lt;/p&gt;

&lt;p&gt;I would have invested in better tooling earlier. A good event viewer that shows the flow of events across services would have saved weeks of debugging time.&lt;/p&gt;

&lt;p&gt;And I would not have migrated everything at once. We tried to move all 10 services in one sprint. It should have been gradual. Start with the least critical services and work toward the most critical.&lt;/p&gt;

&lt;p&gt;Event-driven architecture is powerful but it adds complexity. If you have 3 services that rarely fail direct API calls are probably fine. If you have 10+ services and reliability matters events are worth the investment.&lt;/p&gt;

&lt;p&gt;Have you migrated from direct API calls to event-driven architecture? What surprised you the most?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How defer() in Laravel 11 Made Our API Responses 3x Faster Without Touching a Single Queue</title>
      <dc:creator>Sheikh Shahzaman</dc:creator>
      <pubDate>Sun, 29 Mar 2026 12:17:24 +0000</pubDate>
      <link>https://forem.com/shahzamandev/how-defer-in-laravel-11-made-our-api-responses-3x-faster-without-touching-a-single-queue-4j9o</link>
      <guid>https://forem.com/shahzamandev/how-defer-in-laravel-11-made-our-api-responses-3x-faster-without-touching-a-single-queue-4j9o</guid>
      <description>&lt;p&gt;TL;DR: Laravel 11 introduced defer() which runs code after the HTTP response is sent to the user. No queues, no job classes, no workers. Just wrap your fire-and-forget logic in defer() and your API becomes instantly faster.&lt;/p&gt;




&lt;p&gt;I spent two days last year trying to figure out why our order API endpoint was taking 1.2 seconds to respond. The order itself was being created in about 80ms. So where was the rest of the time going?&lt;/p&gt;

&lt;p&gt;Turns out we were sending a confirmation email, tracking an analytics event, syncing inventory with a third-party service, and clearing a cache key. All of that happened synchronously before the response was sent back to the user.&lt;/p&gt;

&lt;p&gt;The user did not care about any of those things completing before they saw their order confirmation. They just wanted to know their order went through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Way: Queues for Everything
&lt;/h2&gt;

&lt;p&gt;The typical advice is to push these tasks onto a queue. Create a job class, dispatch it, run a queue worker, set up monitoring, handle failed jobs. For a large application with complex background processing that makes sense.&lt;/p&gt;

&lt;p&gt;But for simple fire-and-forget tasks like sending an email or tracking an event? That is a lot of infrastructure for something that should be simple.&lt;/p&gt;

&lt;p&gt;We had 14 different job classes in our application. Eight of them were single-method classes that just did one small thing. Each one had its own file, its own test, and its own entry in the failed jobs table. It felt like overkill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter defer()
&lt;/h2&gt;

&lt;p&gt;Laravel 11 added defer() and it changed how I think about background tasks. Here is how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order_placed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;InventorySync&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:cart"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&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;The response goes back to the user immediately after the order is created. Then Laravel runs all the deferred callbacks after the response is sent. The user never waits for them.&lt;/p&gt;

&lt;p&gt;No job classes. No queue workers. No Redis or database queue driver. Just a closure that runs after the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  When To Use defer() vs Queues
&lt;/h2&gt;

&lt;p&gt;This is the part most articles get wrong. They either say "use defer for everything" or "always use queues." The reality is more nuanced.&lt;/p&gt;

&lt;p&gt;Use defer() when the task is simple and does not need retry logic. Sending a notification email, tracking an analytics event, clearing a cache, logging an activity. If it fails you do not need to retry it automatically.&lt;/p&gt;

&lt;p&gt;Use queues when the task is complex or needs reliability guarantees. Processing a payment, generating a large PDF, syncing thousands of records with an external API. If it fails you need to know about it and retry it.&lt;/p&gt;

&lt;p&gt;I made the mistake of using defer() for a webhook delivery early on. The webhook target was unreliable and about 10% of deliveries failed silently. There was no retry mechanism, no failed job record, no way to know it happened. I moved that back to a queue with 3 retries and the problem was solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Numbers
&lt;/h2&gt;

&lt;p&gt;After refactoring our order endpoint to use defer() for non-critical tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Response time dropped from 1.2 seconds to 280ms&lt;/li&gt;
&lt;li&gt;User-perceived performance improved dramatically&lt;/li&gt;
&lt;li&gt;We deleted 8 single-method job classes&lt;/li&gt;
&lt;li&gt;Queue worker load decreased because fewer jobs were being dispatched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The email still sends. The analytics event still tracks. The cache still clears. The user just does not wait for any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Pattern I Use in Every Project
&lt;/h2&gt;

&lt;p&gt;I created a simple middleware that defers common request-level tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeferCommonTasks&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ActivityLog&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;MetricsCollector&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;trackRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&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;Every request automatically logs activity and tracks metrics without adding any latency to the response. The middleware runs once and every endpoint benefits from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;If I started over I would establish a clear rule from day one: if the task does not need retry logic and takes less than 5 seconds, use defer(). If it needs retries or takes longer, use a queue. Having that rule early would have prevented the 14 unnecessary job classes we ended up with.&lt;/p&gt;

&lt;p&gt;I also would not use defer() inside database transactions. If the transaction rolls back the deferred callback still runs because it executes after the response. This caused a bug where we were sending order confirmation emails for orders that failed to save. I learned that the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;defer() is not a replacement for queues. It is a replacement for the dozens of tiny job classes that exist only because you needed something to run "after" the response.&lt;/p&gt;

&lt;p&gt;One line of code. No infrastructure changes. Measurably faster responses.&lt;/p&gt;

&lt;p&gt;If you are on Laravel 11 or later and you are not using defer() yet you are probably making your users wait for things they do not need to wait for.&lt;/p&gt;

&lt;p&gt;What is the simplest performance win you have found in your Laravel applications?&lt;/p&gt;

</description>
      <category>api</category>
      <category>laravel</category>
      <category>performance</category>
      <category>php</category>
    </item>
    <item>
      <title>How defer() in Laravel 11 Made Our API Responses 3x Faster Without Touching a Single Queue</title>
      <dc:creator>Sheikh Shahzaman</dc:creator>
      <pubDate>Sun, 29 Mar 2026 01:19:09 +0000</pubDate>
      <link>https://forem.com/shahzamandev/how-defer-in-laravel-11-made-our-api-responses-3x-faster-without-touching-a-single-queue-1ogo</link>
      <guid>https://forem.com/shahzamandev/how-defer-in-laravel-11-made-our-api-responses-3x-faster-without-touching-a-single-queue-1ogo</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Laravel 11 introduced defer() which runs code after the HTTP response is sent to the user. No queues, no job classes, no workers. Just wrap your fire-and-forget logic in defer() and your API becomes instantly faster.&lt;/p&gt;




&lt;p&gt;I spent two days last year trying to figure out why our order API endpoint was taking 1.2 seconds to respond. The order itself was being created in about 80ms. So where was the rest of the time going?&lt;/p&gt;

&lt;p&gt;Turns out we were sending a confirmation email, tracking an analytics event, syncing inventory with a third-party service, and clearing a cache key. All of that happened synchronously before the response was sent back to the user.&lt;/p&gt;

&lt;p&gt;The user did not care about any of those things completing before they saw their order confirmation. They just wanted to know their order went through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Way: Queues for Everything
&lt;/h2&gt;

&lt;p&gt;The typical advice is to push these tasks onto a queue. Create a job class, dispatch it, run a queue worker, set up monitoring, handle failed jobs. For a large application with complex background processing that makes sense.&lt;/p&gt;

&lt;p&gt;But for simple fire-and-forget tasks like sending an email or tracking an event? That is a lot of infrastructure for something that should be simple.&lt;/p&gt;

&lt;p&gt;We had 14 different job classes in our application. Eight of them were single-method classes that just did one small thing. Each one had its own file, its own test, and its own entry in the failed jobs table. It felt like overkill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter defer()
&lt;/h2&gt;

&lt;p&gt;Laravel 11 added defer() and it changed how I think about background tasks. Here is how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order_placed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;InventorySync&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:cart"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&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;The response goes back to the user immediately after the order is created. Then Laravel runs all the deferred callbacks after the response is sent. The user never waits for them.&lt;/p&gt;

&lt;p&gt;No job classes. No queue workers. No Redis or database queue driver. Just a closure that runs after the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  When To Use defer() vs Queues
&lt;/h2&gt;

&lt;p&gt;This is the part most articles get wrong. They either say "use defer for everything" or "always use queues." The reality is more nuanced.&lt;/p&gt;

&lt;p&gt;Use defer() when the task is simple and does not need retry logic. Sending a notification email, tracking an analytics event, clearing a cache, logging an activity. If it fails you do not need to retry it automatically.&lt;/p&gt;

&lt;p&gt;Use queues when the task is complex or needs reliability guarantees. Processing a payment, generating a large PDF, syncing thousands of records with an external API. If it fails you need to know about it and retry it.&lt;/p&gt;

&lt;p&gt;I made the mistake of using defer() for a webhook delivery early on. The webhook target was unreliable and about 10% of deliveries failed silently. There was no retry mechanism, no failed job record, no way to know it happened. I moved that back to a queue with 3 retries and the problem was solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Numbers
&lt;/h2&gt;

&lt;p&gt;After refactoring our order endpoint to use defer() for non-critical tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Response time dropped from 1.2 seconds to 280ms&lt;/li&gt;
&lt;li&gt;User-perceived performance improved dramatically&lt;/li&gt;
&lt;li&gt;We deleted 8 single-method job classes&lt;/li&gt;
&lt;li&gt;Queue worker load decreased because fewer jobs were being dispatched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The email still sends. The analytics event still tracks. The cache still clears. The user just does not wait for any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Pattern I Use in Every Project
&lt;/h2&gt;

&lt;p&gt;I created a simple middleware that defers common request-level tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeferCommonTasks&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ActivityLog&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;MetricsCollector&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;trackRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&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;Every request automatically logs activity and tracks metrics without adding any latency to the response. The middleware runs once and every endpoint benefits from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;If I started over I would establish a clear rule from day one: if the task does not need retry logic and takes less than 5 seconds, use defer(). If it needs retries or takes longer, use a queue. Having that rule early would have prevented the 14 unnecessary job classes we ended up with.&lt;/p&gt;

&lt;p&gt;I also would not use defer() inside database transactions. If the transaction rolls back the deferred callback still runs because it executes after the response. This caused a bug where we were sending order confirmation emails for orders that failed to save. I learned that the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;defer() is not a replacement for queues. It is a replacement for the dozens of tiny job classes that exist only because you needed something to run "after" the response.&lt;/p&gt;

&lt;p&gt;One line of code. No infrastructure changes. Measurably faster responses.&lt;/p&gt;

&lt;p&gt;If you are on Laravel 11 or later and you are not using defer() yet you are probably making your users wait for things they do not need to wait for.&lt;/p&gt;

&lt;p&gt;What is the simplest performance win you have found in your Laravel applications?&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
