<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://karthiksukumaran.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://karthiksukumaran.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-10-18T21:04:40+05:30</updated><id>https://karthiksukumaran.com/feed.xml</id><title type="html">Karthik Sukumaran — Architecture Notes</title><subtitle>Practical, architect-level guides to microservices, distributed systems, and GenAI architecture by Karthik Sukumaran.</subtitle><author><name>Karthik Sukumaran</name></author><entry><title type="html">Audit Logging: Recording Who Did What, When, and Why</title><link href="https://karthiksukumaran.com/blog/audit-logging/" rel="alternate" type="text/html" title="Audit Logging: Recording Who Did What, When, and Why" /><published>2026-10-17T09:53:00+05:30</published><updated>2026-10-17T09:53:00+05:30</updated><id>https://karthiksukumaran.com/blog/audit-logging</id><content type="html" xml:base="https://karthiksukumaran.com/blog/audit-logging/"><![CDATA[<p>When a regulator asks <em>“who approved this loan limit increase, and what did they see when they approved it?”</em>, or a security team asks <em>“which accounts did this compromised user view yesterday?”</em>, application logs are not enough. They’re noisy, inconsistently structured, sampled, rotated, and editable by the same teams whose actions are in question.</p>

<p><strong>Audit Logging</strong> produces a <strong>dedicated, structured, durable, tamper-evident record of security- and business-relevant actions</strong>: <em>who</em> did <em>what</em> to <em>which resource</em>, <em>when</em>, from <em>where</em>, with <em>what outcome</em> — and often <em>why</em>.</p>

<h2 id="the-problem">The problem</h2>

<p>Audit requirements come from many directions:</p>

<ul>
  <li><strong>Regulation and standards</strong> — financial regulators, PCI DSS, SOX, HIPAA, ISO 27001, data-protection laws that require logging access to personal data.</li>
  <li><strong>Security</strong> — detecting and investigating account takeover, insider misuse, privilege escalation.</li>
  <li><strong>Disputes and support</strong> — “I never changed my nominee”, “the price was different when I ordered”.</li>
  <li><strong>Operations</strong> — who changed the production configuration before the incident?</li>
</ul>

<p>Application logs fail these needs because they’re optimized for debugging, not evidence.</p>

<h2 id="how-it-works">How it works</h2>

<pre><code class="language-mermaid">flowchart TD
    U[User / service / admin] --&gt; S[Service performs action]
    S --&gt;|same transaction| OB[(Outbox: AuditEvent)]
    OB --&gt; RL[Relay]
    RL --&gt; T[(Audit topic&lt;br/&gt;restricted ACLs)]
    T --&gt; AS[Audit service&lt;br/&gt;validate, enrich, hash-chain]
    AS --&gt; STORE[(Append-only store&lt;br/&gt;WORM / Object Lock)]
    AS --&gt; SIEM[SIEM&lt;br/&gt;Splunk / Sentinel / Elastic Security]
    STORE --&gt; Q[Audit search UI&lt;br/&gt;for compliance &amp; support]
</code></pre>

<p>Core properties of a good audit trail:</p>

<table>
  <thead>
    <tr>
      <th>Property</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Complete</strong></td>
      <td>Every auditable action produces exactly one record — no gaps on failures or retries</td>
    </tr>
    <tr>
      <td><strong>Attributable</strong></td>
      <td>Identifies the real actor (user, service, impersonating admin) and how they authenticated</td>
    </tr>
    <tr>
      <td><strong>Accurate</strong></td>
      <td>Captures what actually happened (outcome), not just what was attempted</td>
    </tr>
    <tr>
      <td><strong>Immutable / tamper-evident</strong></td>
      <td>Records can’t be altered or deleted without detection</td>
    </tr>
    <tr>
      <td><strong>Searchable &amp; retained</strong></td>
      <td>Queryable for the required retention period</td>
    </tr>
    <tr>
      <td><strong>Access-controlled</strong></td>
      <td>Only authorized people can read it; reading the audit log is itself audited</td>
    </tr>
  </tbody>
</table>

<h2 id="what-to-capture">What to capture</h2>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"auditId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"7d3c8e5a-4f21-4b9e-9a0d-61f2b5c0e8a4"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"occurredAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-10-17T09:53:41.218Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"actor"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"emp-20931"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"R. Iyer"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"roles"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"CREDIT_OFFICER"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"authMethod"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SSO+MFA"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"onBehalfOf"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"credit-limit.increase"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"resource"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CardAccount"</span><span class="p">,</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CA-0042117"</span><span class="w"> </span><span class="p">},</span><span class="w">
  </span><span class="nl">"outcome"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SUCCESS"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Customer request ref SR-88123; income proof verified"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"changes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"creditLimit"</span><span class="p">,</span><span class="w"> </span><span class="nl">"from"</span><span class="p">:</span><span class="w"> </span><span class="s2">"150000.00"</span><span class="p">,</span><span class="w"> </span><span class="nl">"to"</span><span class="p">:</span><span class="w"> </span><span class="s2">"250000.00"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"sourceIp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"10.20.4.18"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"userAgent"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BranchPortal/5.4"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"channel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"branch"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"service"</span><span class="p">:</span><span class="w"> </span><span class="s2">"card-limits"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"serviceVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026.10.17"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"traceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"4bf92f3577b34da6a3ce929d0e0e4736"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"approvalId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"APR-2026-55102"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"schemaVersion"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Typical auditable actions:</p>

<ul>
  <li><strong>Authentication</strong> — logins, failures, MFA changes, password resets, session revocations.</li>
  <li><strong>Authorization changes</strong> — role grants, permission changes, admin elevation.</li>
  <li><strong>Sensitive reads</strong> — viewing customer PII, exporting data, downloading statements.</li>
  <li><strong>Business-critical writes</strong> — limit changes, payments, approvals, overrides, refunds, price changes.</li>
  <li><strong>Configuration and operations</strong> — production config changes, feature-flag flips, deployments, key rotations.</li>
</ul>

<p>Capture <strong>failures and denials</strong> too — a run of <code class="language-plaintext highlighter-rouge">ACCESS_DENIED</code> events is often the first sign of an attack.</p>

<h2 id="capturing-reliably">Capturing reliably</h2>

<p>Audit events must not be lost when the action succeeded, nor recorded when it rolled back.</p>

<ul>
  <li><strong>Outbox</strong> — write the audit event in the <strong>same transaction</strong> as the business change (<a href="/blog/transactional-outbox/">Transactional Outbox</a>). The most dependable approach for writes.</li>
  <li><strong>Event sourcing</strong> — if the domain is event-sourced, the event store already <em>is</em> an audit trail of state changes (<a href="/blog/event-sourcing/">Event Sourcing</a>); enrich events with actor and reason metadata.</li>
  <li><strong>Gateway / middleware</strong> — capture sensitive <em>reads</em> and access decisions at a central policy point (API gateway, authorization service) so every endpoint is covered.</li>
  <li><strong>Platform logs</strong> — cloud control-plane logs (AWS CloudTrail, Azure Activity Log, GCP Audit Logs) and Kubernetes audit logs cover infrastructure actions.</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Transactional</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">increaseLimit</span><span class="o">(</span><span class="nc">CardAccountId</span> <span class="n">id</span><span class="o">,</span> <span class="nc">Money</span> <span class="n">newLimit</span><span class="o">,</span> <span class="nc">String</span> <span class="n">reason</span><span class="o">,</span> <span class="nc">Actor</span> <span class="n">actor</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">CardAccount</span> <span class="n">acct</span> <span class="o">=</span> <span class="n">accounts</span><span class="o">.</span><span class="na">load</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
  <span class="nc">Money</span> <span class="n">old</span> <span class="o">=</span> <span class="n">acct</span><span class="o">.</span><span class="na">creditLimit</span><span class="o">();</span>
  <span class="n">acct</span><span class="o">.</span><span class="na">changeLimit</span><span class="o">(</span><span class="n">newLimit</span><span class="o">,</span> <span class="n">actor</span><span class="o">);</span>                     <span class="c1">// domain rules + approvals</span>
  <span class="n">accounts</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">acct</span><span class="o">);</span>

  <span class="n">outbox</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">AuditEvent</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>                       <span class="c1">// same transaction as the change</span>
      <span class="o">.</span><span class="na">actor</span><span class="o">(</span><span class="n">actor</span><span class="o">)</span>
      <span class="o">.</span><span class="na">action</span><span class="o">(</span><span class="s">"credit-limit.increase"</span><span class="o">)</span>
      <span class="o">.</span><span class="na">resource</span><span class="o">(</span><span class="s">"CardAccount"</span><span class="o">,</span> <span class="n">id</span><span class="o">.</span><span class="na">value</span><span class="o">())</span>
      <span class="o">.</span><span class="na">outcome</span><span class="o">(</span><span class="nc">Outcome</span><span class="o">.</span><span class="na">SUCCESS</span><span class="o">)</span>
      <span class="o">.</span><span class="na">reason</span><span class="o">(</span><span class="n">reason</span><span class="o">)</span>
      <span class="o">.</span><span class="na">change</span><span class="o">(</span><span class="s">"creditLimit"</span><span class="o">,</span> <span class="n">old</span><span class="o">.</span><span class="na">toPlainString</span><span class="o">(),</span> <span class="n">newLimit</span><span class="o">.</span><span class="na">toPlainString</span><span class="o">())</span>
      <span class="o">.</span><span class="na">traceId</span><span class="o">(</span><span class="nc">Tracing</span><span class="o">.</span><span class="na">currentTraceId</span><span class="o">())</span>
      <span class="o">.</span><span class="na">build</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="tamper-evidence">Tamper evidence</h2>

<p>Make undetected modification impractical:</p>

<ul>
  <li><strong>Append-only storage</strong> with retention locks — S3 Object Lock (compliance mode), Azure immutable blob storage, WORM-capable archives.</li>
  <li><strong>Hash chaining</strong> — each record includes the hash of the previous record; altering one breaks the chain from that point on.</li>
  <li><strong>Periodic anchoring</strong> — sign or publish the latest chain hash (daily digest) to a separate system so the chain itself can’t be rewritten wholesale.</li>
  <li><strong>Separation of duties</strong> — application teams can write audit events but not modify or delete them; a separate team administers the audit store.</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">hashlib</span><span class="p">,</span> <span class="n">json</span>

<span class="k">def</span> <span class="nf">chain</span><span class="p">(</span><span class="n">prev_hash</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">record</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="n">body</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">record</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">separators</span><span class="o">=</span><span class="p">(</span><span class="s">","</span><span class="p">,</span> <span class="s">":"</span><span class="p">))</span>
    <span class="n">record</span><span class="p">[</span><span class="s">"prevHash"</span><span class="p">]</span> <span class="o">=</span> <span class="n">prev_hash</span>
    <span class="n">record</span><span class="p">[</span><span class="s">"hash"</span><span class="p">]</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">sha256</span><span class="p">((</span><span class="n">prev_hash</span> <span class="o">+</span> <span class="n">body</span><span class="p">).</span><span class="n">encode</span><span class="p">()).</span><span class="n">hexdigest</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">record</span>

<span class="k">def</span> <span class="nf">verify</span><span class="p">(</span><span class="n">records</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="n">prev</span> <span class="o">=</span> <span class="s">"GENESIS"</span>
    <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">records</span><span class="p">:</span>
        <span class="n">body</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">r</span><span class="p">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="n">k</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="s">"prevHash"</span><span class="p">,</span> <span class="s">"hash"</span><span class="p">)}</span>
        <span class="n">expected</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">sha256</span><span class="p">((</span><span class="n">prev</span> <span class="o">+</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">body</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">separators</span><span class="o">=</span><span class="p">(</span><span class="s">","</span><span class="p">,</span> <span class="s">":"</span><span class="p">))).</span><span class="n">encode</span><span class="p">()).</span><span class="n">hexdigest</span><span class="p">()</span>
        <span class="k">if</span> <span class="n">r</span><span class="p">[</span><span class="s">"prevHash"</span><span class="p">]</span> <span class="o">!=</span> <span class="n">prev</span> <span class="ow">or</span> <span class="n">r</span><span class="p">[</span><span class="s">"hash"</span><span class="p">]</span> <span class="o">!=</span> <span class="n">expected</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">False</span>
        <span class="n">prev</span> <span class="o">=</span> <span class="n">r</span><span class="p">[</span><span class="s">"hash"</span><span class="p">]</span>
    <span class="k">return</span> <span class="bp">True</span>
</code></pre></div></div>

<p><em>(Per-stream chains — per tenant or per day — keep this scalable; a single global chain becomes a serialization bottleneck.)</em></p>

<h2 id="audit-logs-vs-application-logs">Audit logs vs. application logs</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Application logs</th>
      <th>Audit logs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Purpose</td>
      <td>Debugging, operations</td>
      <td>Accountability, compliance, forensics</td>
    </tr>
    <tr>
      <td>Audience</td>
      <td>Engineers</td>
      <td>Auditors, security, compliance, support</td>
    </tr>
    <tr>
      <td>Completeness</td>
      <td>Best effort; sampled; levels</td>
      <td>Every auditable action, guaranteed</td>
    </tr>
    <tr>
      <td>Schema</td>
      <td>Loose</td>
      <td>Strict, versioned</td>
    </tr>
    <tr>
      <td>Retention</td>
      <td>Days to weeks</td>
      <td>Years (per regulation)</td>
    </tr>
    <tr>
      <td>Mutability</td>
      <td>Rotated, deleted</td>
      <td>Append-only, tamper-evident</td>
    </tr>
    <tr>
      <td>Access</td>
      <td>Broad within engineering</td>
      <td>Restricted; access itself audited</td>
    </tr>
  </tbody>
</table>

<h2 id="privacy">Privacy</h2>

<p>Audit logs often contain personal data and must themselves comply with data-protection rules:</p>

<ul>
  <li><strong>Minimize</strong> — record identifiers and changed fields, not entire records.</li>
  <li><strong>Mask</strong> sensitive values (card numbers, national IDs) in <code class="language-plaintext highlighter-rouge">changes</code>.</li>
  <li><strong>Legal basis and retention</strong> — keep for the legally required period, then delete or anonymize.</li>
  <li><strong>Erasure conflicts</strong> — where audit retention obligations override erasure requests, document the basis; consider pseudonymous actor IDs with a separately managed mapping.</li>
</ul>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Accountability and regulatory compliance</td>
      <td>Storage and long-term retention costs</td>
    </tr>
    <tr>
      <td>Faster security investigations</td>
      <td>Engineering effort to cover every auditable action</td>
    </tr>
    <tr>
      <td>Evidence for disputes and support</td>
      <td>Privacy obligations for the audit data itself</td>
    </tr>
    <tr>
      <td>Deterrent against misuse</td>
      <td>Operational separation of duties to maintain</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Warning:</strong> auditing via application <code class="language-plaintext highlighter-rouge">log.info()</code>. When log sampling, level changes, or rotation drop the line, the audit record is gone — and engineers can edit or delete it. Audit events need their own reliable path and storage.</p>
</blockquote>

<ul>
  <li><strong>Recording the attempt, not the outcome</strong> — “user requested limit increase” without whether it succeeded.</li>
  <li><strong>Losing the real actor</strong> behind service accounts — propagate the end-user identity (and any impersonation) through service calls.</li>
  <li><strong>No audit of reads</strong> for sensitive data, which many regulations explicitly require.</li>
  <li><strong>Free-text actions</strong> (<code class="language-plaintext highlighter-rouge">"changed stuff"</code>) instead of a controlled vocabulary.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>Capture</strong> — outbox + Kafka, event stores, API gateway and policy-engine logs (OPA decision logs), Kubernetes audit logs, AWS CloudTrail / Azure Activity Logs / GCP Cloud Audit Logs.</li>
  <li><strong>Storage</strong> — S3 Object Lock, Azure immutable storage, append-only databases or ledger tables (e.g., SQL Server ledger tables), dedicated audit services.</li>
  <li><strong>Analysis</strong> — SIEM platforms such as Splunk, Microsoft Sentinel, Elastic Security, Google Security Operations.</li>
</ul>

<p>In banking environments I’ve worked in, audit trails for document access and customer-data changes were as important as the features themselves — designed in from the first sprint and verified in every compliance review.</p>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/event-sourcing/">Event Sourcing</a> — state changes as a built-in audit trail.</li>
  <li><a href="/blog/transactional-outbox/">Transactional Outbox</a> — reliable audit event capture.</li>
  <li><a href="/blog/log-aggregation/">Log Aggregation</a> — operational logs, kept separate from audit.</li>
  <li><a href="/blog/correlation-id/">Correlation ID</a> and <a href="/blog/distributed-tracing/">Distributed Tracing</a> — linking audit events to the requests that caused them.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>Audit logs answer who did what to which resource, when, from where, with what outcome — and why.</li>
  <li>Capture them reliably (outbox, event store, central policy point), including failures and sensitive reads.</li>
  <li>Store them append-only and tamper-evident, with separation of duties.</li>
  <li>Treat the audit trail as regulated data: strict schema, restricted access, defined retention.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Observability" /><category term="audit-logging" /><category term="compliance" /><category term="siem" /><category term="tamper-evident" /><category term="security" /><category term="governance" /><summary type="html"><![CDATA[Audit Logging pattern explained: records who did what and when. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Application Metrics: Measuring Latency, Errors, and Throughput</title><link href="https://karthiksukumaran.com/blog/application-metrics/" rel="alternate" type="text/html" title="Application Metrics: Measuring Latency, Errors, and Throughput" /><published>2026-10-17T09:52:00+05:30</published><updated>2026-10-17T09:52:00+05:30</updated><id>https://karthiksukumaran.com/blog/application-metrics</id><content type="html" xml:base="https://karthiksukumaran.com/blog/application-metrics/"><![CDATA[<p>Logs tell stories; traces show journeys; <strong>metrics show trends</strong>. A metric is a <strong>numeric measurement aggregated over time</strong> — requests per second, error ratio, p99 latency, queue depth, connection-pool usage. Metrics are cheap to store, fast to query, and ideal for <strong>dashboards and alerts</strong>: they answer <em>“is the system healthy right now, and how does that compare to yesterday?”</em></p>

<h2 id="the-problem">The problem</h2>

<p>Without metrics:</p>

<ul>
  <li>you learn about outages from customers,</li>
  <li>you can’t tell whether a deploy made things worse,</li>
  <li>capacity planning is guesswork,</li>
  <li>and “is it slow?” is a matter of opinion.</li>
</ul>

<p>Logs can’t replace metrics at scale: counting events by scanning logs is slow and expensive, and log-based alerts are brittle.</p>

<h2 id="what-to-measure">What to measure</h2>

<h3 id="the-red-method-for-request-driven-services">The RED method (for request-driven services)</h3>

<table>
  <thead>
    <tr>
      <th>Signal</th>
      <th>Meaning</th>
      <th>Example metric</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>R</strong>ate</td>
      <td>Requests per second</td>
      <td><code class="language-plaintext highlighter-rouge">http_server_requests_seconds_count</code></td>
    </tr>
    <tr>
      <td><strong>E</strong>rrors</td>
      <td>Failed requests per second (or ratio)</td>
      <td>same, filtered by <code class="language-plaintext highlighter-rouge">status=~"5.."</code></td>
    </tr>
    <tr>
      <td><strong>D</strong>uration</td>
      <td>Latency distribution</td>
      <td><code class="language-plaintext highlighter-rouge">http_server_requests_seconds_bucket</code> (histogram)</td>
    </tr>
  </tbody>
</table>

<h3 id="the-use-method-for-resources">The USE method (for resources)</h3>

<table>
  <thead>
    <tr>
      <th>Signal</th>
      <th>Meaning</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>U</strong>tilization</td>
      <td>% of time the resource is busy</td>
      <td>CPU, connection-pool active / max</td>
    </tr>
    <tr>
      <td><strong>S</strong>aturation</td>
      <td>Queued work the resource can’t serve yet</td>
      <td>Run queue, pool pending threads, Kafka consumer lag</td>
    </tr>
    <tr>
      <td><strong>E</strong>rrors</td>
      <td>Resource-level errors</td>
      <td>Pool timeouts, disk errors</td>
    </tr>
  </tbody>
</table>

<p>Google’s <strong>four golden signals</strong> — latency, traffic, errors, saturation — combine both views.</p>

<h3 id="business-metrics">Business metrics</h3>

<p>Technical metrics can look fine while the business is broken. Add a few <strong>business KPIs</strong>: orders placed per minute, payment success ratio, sign-ups, statement generation completed. A sudden drop in orders is often the fastest outage detector.</p>

<h2 id="metric-types">Metric types</h2>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Behavior</th>
      <th>Use for</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Counter</strong></td>
      <td>Only increases (resets on restart)</td>
      <td>Events: requests, errors, messages</td>
      <td><code class="language-plaintext highlighter-rouge">orders_placed_total</code></td>
    </tr>
    <tr>
      <td><strong>Gauge</strong></td>
      <td>Goes up and down</td>
      <td>Current values: queue depth, in-flight, pool size</td>
      <td><code class="language-plaintext highlighter-rouge">db_pool_active_connections</code></td>
    </tr>
    <tr>
      <td><strong>Histogram</strong></td>
      <td>Counts observations into buckets</td>
      <td>Latency and size distributions; percentiles across instances</td>
      <td><code class="language-plaintext highlighter-rouge">http_request_duration_seconds</code></td>
    </tr>
    <tr>
      <td><strong>Summary</strong></td>
      <td>Client-side quantiles</td>
      <td>Rarely preferred — quantiles can’t be aggregated across instances</td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Rule of thumb:</strong> use <strong>histograms</strong> for latency. You can compute percentiles across all instances (<code class="language-plaintext highlighter-rouge">histogram_quantile</code>), whereas averaging per-instance p99s is mathematically meaningless.</p>
</blockquote>

<h2 id="example-micrometer-spring-boot">Example: Micrometer (Spring Boot)</h2>

<p>Spring Boot Actuator with Micrometer exposes HTTP, JVM, pool, and Kafka metrics automatically at <code class="language-plaintext highlighter-rouge">/actuator/prometheus</code>. Add domain metrics explicitly:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PaymentService</span> <span class="o">{</span>

  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Counter</span> <span class="n">captured</span><span class="o">;</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Counter</span> <span class="n">declined</span><span class="o">;</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Timer</span> <span class="n">gatewayLatency</span><span class="o">;</span>

  <span class="kd">public</span> <span class="nf">PaymentService</span><span class="o">(</span><span class="nc">MeterRegistry</span> <span class="n">registry</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">this</span><span class="o">.</span><span class="na">captured</span> <span class="o">=</span> <span class="nc">Counter</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"payments_captured_total"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">description</span><span class="o">(</span><span class="s">"Payments successfully captured"</span><span class="o">).</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>
    <span class="k">this</span><span class="o">.</span><span class="na">declined</span> <span class="o">=</span> <span class="nc">Counter</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"payments_declined_total"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">tag</span><span class="o">(</span><span class="s">"reason"</span><span class="o">,</span> <span class="s">"unknown"</span><span class="o">).</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>        <span class="c1">// see note on tags below</span>
    <span class="k">this</span><span class="o">.</span><span class="na">gatewayLatency</span> <span class="o">=</span> <span class="nc">Timer</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"payment_gateway_duration"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">publishPercentileHistogram</span><span class="o">()</span>                         <span class="c1">// export histogram buckets</span>
        <span class="o">.</span><span class="na">serviceLevelObjectives</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMillis</span><span class="o">(</span><span class="mi">300</span><span class="o">),</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">1</span><span class="o">))</span>
        <span class="o">.</span><span class="na">tag</span><span class="o">(</span><span class="s">"gateway"</span><span class="o">,</span> <span class="s">"primary"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="nc">PaymentResult</span> <span class="nf">capture</span><span class="o">(</span><span class="nc">Payment</span> <span class="n">p</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">PaymentResult</span> <span class="n">r</span> <span class="o">=</span> <span class="n">gatewayLatency</span><span class="o">.</span><span class="na">record</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">gateway</span><span class="o">.</span><span class="na">capture</span><span class="o">(</span><span class="n">p</span><span class="o">));</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">r</span><span class="o">.</span><span class="na">isSuccess</span><span class="o">())</span> <span class="n">captured</span><span class="o">.</span><span class="na">increment</span><span class="o">();</span> <span class="k">else</span> <span class="n">declined</span><span class="o">.</span><span class="na">increment</span><span class="o">();</span>
    <span class="k">return</span> <span class="n">r</span><span class="o">;</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">management</span><span class="pi">:</span>
  <span class="na">endpoints.web.exposure.include</span><span class="pi">:</span> <span class="s">health,prometheus</span>
  <span class="na">metrics</span><span class="pi">:</span>
    <span class="na">tags</span><span class="pi">:</span>                                  <span class="c1"># common tags on every metric</span>
      <span class="na">service</span><span class="pi">:</span> <span class="s">payments</span>
      <span class="na">env</span><span class="pi">:</span> <span class="s">prod</span>
    <span class="na">distribution</span><span class="pi">:</span>
      <span class="na">percentiles-histogram</span><span class="pi">:</span>
        <span class="na">http.server.requests</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<h2 id="example-prometheus-queries">Example: Prometheus queries</h2>

<pre><code class="language-promql"># Request rate per route (RED: Rate)
sum by (uri) (rate(http_server_requests_seconds_count{service="payments"}[5m]))

# Error ratio (RED: Errors)
sum(rate(http_server_requests_seconds_count{service="payments", status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{service="payments"}[5m]))

# p99 latency across all instances (RED: Duration)
histogram_quantile(0.99,
  sum by (le) (rate(http_server_requests_seconds_bucket{service="payments"}[5m])))

# Saturation: Kafka consumer lag for the ledger group
sum by (topic) (kafka_consumergroup_lag{consumergroup="ledger"})
</code></pre>

<h2 id="slos-and-alerting">SLOs and alerting</h2>

<p>Alert on <strong>symptoms users feel</strong>, not on every cause. Define <strong>Service Level Objectives</strong> and alert when you’re burning the <strong>error budget</strong> too fast.</p>

<pre><code class="language-mermaid">flowchart TD
    SLI["SLI: % of checkout requests&lt;br/&gt;successful and &lt; 800 ms"] --&gt; SLO["SLO: 99.9% over 30 days"]
    SLO --&gt; EB["Error budget: 0.1%&lt;br/&gt;≈ 43 min of 'bad' per month"]
    EB --&gt; FAST["Fast burn alert&lt;br/&gt;budget burning 14× → page now"]
    EB --&gt; SLOW["Slow burn alert&lt;br/&gt;budget burning 3× over 6 h → ticket"]
</code></pre>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Prometheus alerting rule: multi-window burn rate (page)</span>
<span class="na">groups</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">checkout-slo</span>
    <span class="na">rules</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">alert</span><span class="pi">:</span> <span class="s">CheckoutErrorBudgetFastBurn</span>
        <span class="na">expr</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">(</span>
            <span class="s">sum(rate(http_server_requests_seconds_count{service="checkout",status=~"5.."}[5m]))</span>
            <span class="s">/ sum(rate(http_server_requests_seconds_count{service="checkout"}[5m]))</span>
          <span class="s">) &gt; (14.4 * 0.001)</span>
          <span class="s">and</span>
          <span class="s">(</span>
            <span class="s">sum(rate(http_server_requests_seconds_count{service="checkout",status=~"5.."}[1h]))</span>
            <span class="s">/ sum(rate(http_server_requests_seconds_count{service="checkout"}[1h]))</span>
          <span class="s">) &gt; (14.4 * 0.001)</span>
        <span class="na">for</span><span class="pi">:</span> <span class="s">2m</span>
        <span class="na">labels</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">severity</span><span class="pi">:</span> <span class="nv">page</span> <span class="pi">}</span>
        <span class="na">annotations</span><span class="pi">:</span>
          <span class="na">summary</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Checkout</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">burning</span><span class="nv"> </span><span class="s">its</span><span class="nv"> </span><span class="s">30-day</span><span class="nv"> </span><span class="s">error</span><span class="nv"> </span><span class="s">budget</span><span class="nv"> </span><span class="s">~14x</span><span class="nv"> </span><span class="s">too</span><span class="nv"> </span><span class="s">fast"</span>
          <span class="na">runbook</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://runbooks.example.com/checkout/error-budget"</span>
</code></pre></div></div>

<p>The two windows (5 m and 1 h) make the alert both <strong>fast</strong> and <strong>resistant to blips</strong> — the approach described in Google’s SRE Workbook.</p>

<h2 id="cardinality-the-silent-killer">Cardinality: the silent killer</h2>

<p>Every unique combination of label values is a separate time series. Labels like <code class="language-plaintext highlighter-rouge">user_id</code>, <code class="language-plaintext highlighter-rouge">order_id</code>, <code class="language-plaintext highlighter-rouge">email</code>, or raw URL paths create <strong>millions</strong> of series and can take down your metrics backend.</p>

<table>
  <thead>
    <tr>
      <th>Good labels (bounded)</th>
      <th>Bad labels (unbounded)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">service</code>, <code class="language-plaintext highlighter-rouge">env</code>, <code class="language-plaintext highlighter-rouge">region</code>, <code class="language-plaintext highlighter-rouge">status_class</code></td>
      <td><code class="language-plaintext highlighter-rouge">user_id</code>, <code class="language-plaintext highlighter-rouge">order_id</code>, <code class="language-plaintext highlighter-rouge">session_id</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">route</code> template (<code class="language-plaintext highlighter-rouge">/orders/{id}</code>)</td>
      <td>Raw path (<code class="language-plaintext highlighter-rouge">/orders/88121</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">method</code>, <code class="language-plaintext highlighter-rouge">outcome</code>, <code class="language-plaintext highlighter-rouge">gateway</code></td>
      <td>Free-text error messages</td>
    </tr>
  </tbody>
</table>

<p>Put unbounded identifiers in <strong>logs and traces</strong>; use <strong>exemplars</strong> to link a metric data point to a sample trace.</p>

<h2 id="dashboards-that-help">Dashboards that help</h2>

<ul>
  <li><strong>One service overview</strong> per service: RED panels, saturation, dependencies, deploy markers.</li>
  <li><strong>Top-down</strong> — start from user journeys (checkout, login) and drill into services.</li>
  <li><strong>Consistent layouts and units</strong> across teams.</li>
  <li><strong>Annotations</strong> for deployments and incidents, so cause and effect are visible.</li>
</ul>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cheap, fast, ideal for alerting and trends</td>
      <td>Aggregated — loses per-request detail</td>
    </tr>
    <tr>
      <td>Clear SLOs and error budgets</td>
      <td>Cardinality must be governed</td>
    </tr>
    <tr>
      <td>Capacity planning and regression detection</td>
      <td>Histogram bucket choices affect accuracy</td>
    </tr>
    <tr>
      <td>Business KPIs alongside technical signals</td>
      <td>Dashboards sprawl without curation</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> alerting on causes (CPU &gt; 80%, one pod restarted) instead of symptoms (error budget burning). You get paged constantly for things users never notice, and alert fatigue hides the real incidents.</p>
</blockquote>

<ul>
  <li><strong>Averages for latency</strong> — hide the tail; use percentiles from histograms.</li>
  <li><strong>Counters reset on restart</strong> — always use <code class="language-plaintext highlighter-rouge">rate()</code>/<code class="language-plaintext highlighter-rouge">increase()</code>, never raw counter values.</li>
  <li><strong>Missing saturation metrics</strong> — pools, queues, and lags warn you before errors start.</li>
  <li><strong>Dashboards nobody owns</strong> — review and prune them.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>Prometheus</strong> (+ Alertmanager), <strong>Grafana</strong>, <strong>Thanos / Cortex / Mimir / VictoriaMetrics</strong> for long-term, multi-cluster storage.</li>
  <li><strong>Instrumentation</strong> — Micrometer (Java), OpenTelemetry Metrics (all languages), <code class="language-plaintext highlighter-rouge">prometheus_client</code> libraries, .NET <code class="language-plaintext highlighter-rouge">System.Diagnostics.Metrics</code>.</li>
  <li><strong>SaaS</strong> — Datadog, New Relic, Dynatrace, Grafana Cloud, cloud-native monitoring (CloudWatch, Azure Monitor, Cloud Monitoring).</li>
</ul>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/distributed-tracing/">Distributed Tracing</a> — exemplars link metrics to traces.</li>
  <li><a href="/blog/log-aggregation/">Log Aggregation</a> — investigate after a metric alert fires.</li>
  <li><a href="/blog/health-check/">Health Check</a> — binary health vs. rich metrics.</li>
  <li><a href="/blog/dead-letter-queue/">Dead Letter Queue</a> and <a href="/blog/circuit-breaker/">Circuit Breaker</a> — DLQ depth and breaker state are key metrics.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>Measure RED for services, USE for resources, plus a few business KPIs.</li>
  <li>Use counters, gauges, and histograms; compute percentiles from histograms.</li>
  <li>Alert on SLO burn rates, not on every cause.</li>
  <li>Keep label cardinality bounded; put IDs in logs and traces.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Observability" /><category term="metrics" /><category term="prometheus" /><category term="grafana" /><category term="red-method" /><category term="slo" /><category term="histograms" /><category term="alerting" /><summary type="html"><![CDATA[Application Metrics pattern explained: tracks latency, errors, throughput. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Log Aggregation: All Your Logs, One Searchable Place</title><link href="https://karthiksukumaran.com/blog/log-aggregation/" rel="alternate" type="text/html" title="Log Aggregation: All Your Logs, One Searchable Place" /><published>2026-10-17T09:51:00+05:30</published><updated>2026-10-17T09:51:00+05:30</updated><id>https://karthiksukumaran.com/blog/log-aggregation</id><content type="html" xml:base="https://karthiksukumaran.com/blog/log-aggregation/"><![CDATA[<p>With one server, you <code class="language-plaintext highlighter-rouge">ssh</code> in and <code class="language-plaintext highlighter-rouge">tail -f</code> the log. With 300 pods that come and go every hour, that’s impossible: logs are spread across nodes, and they <strong>vanish when a container is replaced</strong>. <strong>Log Aggregation</strong> collects logs from every service and instance into a <strong>central, searchable, retained store</strong>, so you can investigate any request, any time, across the whole system.</p>

<h2 id="the-problem">The problem</h2>

<ul>
  <li>Container logs are ephemeral — a crashed pod takes its logs with it.</li>
  <li>One request’s story spans many services (<a href="/blog/correlation-id/">Correlation ID</a> only helps if logs are searchable together).</li>
  <li>Unstructured text logs are slow and unreliable to search.</li>
  <li>Compliance and security teams need retained, tamper-resistant logs.</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<pre><code class="language-mermaid">flowchart TD
    subgraph Node["Each Kubernetes node"]
      P1[Pod A&lt;br/&gt;JSON to stdout] --&gt; F[/var/log/containers/*.log/]
      P2[Pod B&lt;br/&gt;JSON to stdout] --&gt; F
      F --&gt; AG[Agent DaemonSet&lt;br/&gt;Fluent Bit / OTel Collector / Vector]
    end
    AG --&gt;|"enrich: k8s labels, namespace, node"| BUF[(Buffer&lt;br/&gt;Kafka — optional)]
    BUF --&gt; PROC[Processing&lt;br/&gt;parse, redact, route, drop noise]
    PROC --&gt; HOT[(Hot store&lt;br/&gt;Elasticsearch / Loki / Splunk)]
    PROC --&gt; COLD[(Archive&lt;br/&gt;object storage)]
    HOT --&gt; UI[Kibana / Grafana / Splunk search]
</code></pre>

<ol>
  <li><strong>Emit</strong> — applications write <strong>structured logs</strong> (JSON) to <strong>stdout/stderr</strong>.</li>
  <li><strong>Collect</strong> — a node-level agent tails container log files (or receives OTLP logs), adding metadata: service, namespace, pod, node, version.</li>
  <li><strong>Process</strong> — parse, normalize fields, redact sensitive data, drop noise, route by type.</li>
  <li><strong>Store</strong> — index in a hot store for fast search; archive to cheap object storage for long retention.</li>
  <li><strong>Search, alert, and correlate</strong> — query by fields, pivot to traces via <code class="language-plaintext highlighter-rouge">trace_id</code>.</li>
</ol>

<h2 id="structured-logging">Structured logging</h2>

<p>Structured logs are the single biggest improvement you can make.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"ts"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-10-17T09:51:12.384Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"level"</span><span class="p">:</span><span class="w"> </span><span class="s2">"WARN"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"service"</span><span class="p">:</span><span class="w"> </span><span class="s2">"payments"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026.10.17"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="s2">"prod"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"trace_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"4bf92f3577b34da6a3ce929d0e0e4736"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"span_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"00f067aa0ba902b7"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"correlation_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"checkout-7f3a"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"event"</span><span class="p">:</span><span class="w"> </span><span class="s2">"payment.gateway.retry"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Gateway timeout, retrying"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"attempt"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"gateway"</span><span class="p">:</span><span class="w"> </span><span class="s2">"primary"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"duration_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">2003</span><span class="p">,</span><span class="w">
  </span><span class="nl">"order_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ord-55120"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Guidelines:</p>

<ul>
  <li><strong>Consistent field names</strong> across services (adopt OpenTelemetry semantic conventions or ECS — Elastic Common Schema).</li>
  <li><strong>An <code class="language-plaintext highlighter-rouge">event</code> name</strong> for each log type (<code class="language-plaintext highlighter-rouge">payment.gateway.retry</code>) — searchable and countable.</li>
  <li><strong>IDs as fields</strong> — <code class="language-plaintext highlighter-rouge">trace_id</code>, <code class="language-plaintext highlighter-rouge">correlation_id</code>, business IDs (order, account) where policy allows.</li>
  <li><strong>Levels used deliberately</strong> — <code class="language-plaintext highlighter-rouge">ERROR</code> means someone should look; <code class="language-plaintext highlighter-rouge">INFO</code> for business-significant events; <code class="language-plaintext highlighter-rouge">DEBUG</code> off in production by default.</li>
  <li><strong>No secrets, tokens, passwords, card numbers, or unnecessary personal data.</strong></li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Python: structlog with JSON output and trace correlation
</span><span class="kn">import</span> <span class="nn">structlog</span>
<span class="kn">from</span> <span class="nn">opentelemetry</span> <span class="kn">import</span> <span class="n">trace</span>

<span class="k">def</span> <span class="nf">add_trace</span><span class="p">(</span><span class="n">_</span><span class="p">,</span> <span class="n">__</span><span class="p">,</span> <span class="n">event_dict</span><span class="p">):</span>
    <span class="n">ctx</span> <span class="o">=</span> <span class="n">trace</span><span class="p">.</span><span class="n">get_current_span</span><span class="p">().</span><span class="n">get_span_context</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">ctx</span><span class="p">.</span><span class="n">is_valid</span><span class="p">:</span>
        <span class="n">event_dict</span><span class="p">[</span><span class="s">"trace_id"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">format</span><span class="p">(</span><span class="n">ctx</span><span class="p">.</span><span class="n">trace_id</span><span class="p">,</span> <span class="s">"032x"</span><span class="p">)</span>
        <span class="n">event_dict</span><span class="p">[</span><span class="s">"span_id"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">format</span><span class="p">(</span><span class="n">ctx</span><span class="p">.</span><span class="n">span_id</span><span class="p">,</span> <span class="s">"016x"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">event_dict</span>

<span class="n">structlog</span><span class="p">.</span><span class="n">configure</span><span class="p">(</span><span class="n">processors</span><span class="o">=</span><span class="p">[</span>
    <span class="n">structlog</span><span class="p">.</span><span class="n">contextvars</span><span class="p">.</span><span class="n">merge_contextvars</span><span class="p">,</span>
    <span class="n">structlog</span><span class="p">.</span><span class="n">processors</span><span class="p">.</span><span class="n">add_log_level</span><span class="p">,</span>
    <span class="n">structlog</span><span class="p">.</span><span class="n">processors</span><span class="p">.</span><span class="n">TimeStamper</span><span class="p">(</span><span class="n">fmt</span><span class="o">=</span><span class="s">"iso"</span><span class="p">,</span> <span class="n">utc</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="s">"ts"</span><span class="p">),</span>
    <span class="n">add_trace</span><span class="p">,</span>
    <span class="n">structlog</span><span class="p">.</span><span class="n">processors</span><span class="p">.</span><span class="n">JSONRenderer</span><span class="p">(),</span>
<span class="p">])</span>

<span class="n">log</span> <span class="o">=</span> <span class="n">structlog</span><span class="p">.</span><span class="n">get_logger</span><span class="p">(</span><span class="n">service</span><span class="o">=</span><span class="s">"payments"</span><span class="p">)</span>
<span class="n">log</span><span class="p">.</span><span class="n">warning</span><span class="p">(</span><span class="s">"payment.gateway.retry"</span><span class="p">,</span> <span class="n">attempt</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">gateway</span><span class="o">=</span><span class="s">"primary"</span><span class="p">,</span> <span class="n">duration_ms</span><span class="o">=</span><span class="mi">2003</span><span class="p">,</span> <span class="n">order_id</span><span class="o">=</span><span class="s">"ord-55120"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="collection-fluent-bit-example">Collection: Fluent Bit example</h2>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[INPUT]</span>
    <span class="err">Name</span>              <span class="err">tail</span>
    <span class="err">Path</span>              <span class="err">/var/log/containers/*.log</span>
    <span class="err">multiline.parser</span>  <span class="err">docker,</span> <span class="err">cri</span>
    <span class="err">Tag</span>               <span class="err">kube.*</span>
    <span class="err">Mem_Buf_Limit</span>     <span class="err">50MB</span>
    <span class="err">Skip_Long_Lines</span>   <span class="err">On</span>

<span class="nn">[FILTER]</span>
    <span class="err">Name</span>              <span class="err">kubernetes</span>
    <span class="err">Match</span>             <span class="err">kube.*</span>
    <span class="err">Merge_Log</span>         <span class="err">On</span>          <span class="c"># parse JSON app logs into fields
</span>    <span class="err">Keep_Log</span>          <span class="err">Off</span>
    <span class="err">Labels</span>            <span class="err">On</span>

<span class="nn">[FILTER]</span>
    <span class="err">Name</span>              <span class="err">modify</span>      <span class="c"># redact obvious secrets defensively
</span>    <span class="err">Match</span>             <span class="err">kube.*</span>
    <span class="err">Remove</span>            <span class="err">authorization</span>
    <span class="err">Remove</span>            <span class="err">password</span>

<span class="nn">[FILTER]</span>
    <span class="err">Name</span>              <span class="err">grep</span>        <span class="c"># drop noisy health-check access logs
</span>    <span class="err">Match</span>             <span class="err">kube.*</span>
    <span class="err">Exclude</span>           <span class="err">path</span> <span class="err">^/actuator/health</span>

<span class="nn">[OUTPUT]</span>
    <span class="err">Name</span>              <span class="err">loki</span>
    <span class="err">Match</span>             <span class="err">kube.*</span>
    <span class="err">Host</span>              <span class="err">loki-gateway.observability</span>
    <span class="err">Labels</span>            <span class="py">job</span><span class="p">=</span><span class="s">fluent-bit, namespace=$kubernetes['namespace_name'], app=$kubernetes['labels']['app']</span>
</code></pre></div></div>

<h2 id="choosing-a-backend">Choosing a backend</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Elasticsearch / OpenSearch (ELK)</th>
      <th>Grafana Loki</th>
      <th>Splunk / commercial SaaS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Indexing</td>
      <td>Full-text index on fields</td>
      <td>Indexes <strong>labels only</strong>; content scanned at query time</td>
      <td>Full indexing, rich analytics</td>
    </tr>
    <tr>
      <td>Query power</td>
      <td>Very high (full-text, aggregations)</td>
      <td>Good with LogQL; best when filtered by labels</td>
      <td>Very high (SPL), mature security use cases</td>
    </tr>
    <tr>
      <td>Cost profile</td>
      <td>Storage + compute for indexes</td>
      <td>Cheap storage (object store), lower index cost</td>
      <td>License/ingest-based; can be expensive</td>
    </tr>
    <tr>
      <td>Best for</td>
      <td>Search-heavy investigations, security analytics</td>
      <td>High-volume Kubernetes logs, cost efficiency</td>
      <td>Enterprise SOC, compliance, mixed estates</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>In practice:</strong> Loki labels should be <strong>low-cardinality</strong> (namespace, app, level). Putting <code class="language-plaintext highlighter-rouge">order_id</code> or <code class="language-plaintext highlighter-rouge">user_id</code> in labels explodes the index; keep those as fields in the log line.</p>
</blockquote>

<h2 id="retention-cost-and-noise">Retention, cost, and noise</h2>

<p>Logs are often the largest observability bill. Control it deliberately:</p>

<ul>
  <li><strong>Tiered retention</strong> — e.g., 7–14 days hot, 90 days warm, 1–7 years archived where regulation requires.</li>
  <li><strong>Drop or sample noise</strong> — health checks, successful debug chatter, repetitive warnings.</li>
  <li><strong>Log levels per environment</strong> and dynamic level changes for incidents.</li>
  <li><strong>Prefer metrics for counts</strong> — don’t log every request just to count them; that’s what <a href="/blog/application-metrics/">Application Metrics</a> are for.</li>
  <li><strong>Budget by service</strong> — show teams their log volume and cost.</li>
</ul>

<h2 id="security-and-compliance">Security and compliance</h2>

<ul>
  <li><strong>Redact at the source</strong> first; filters are a safety net.</li>
  <li><strong>Access control</strong> — logs often contain personal data; restrict by team and environment.</li>
  <li><strong>Integrity</strong> — security-relevant logs may need tamper-evident storage (WORM object storage) and separation from application teams. For “who did what” records, see <a href="/blog/audit-logging/">Audit Logging</a>.</li>
  <li><strong>Data residency</strong> — where logs are stored matters under data-protection laws.</li>
</ul>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Search across every service and instance</td>
      <td>Storage and ingestion cost can be large</td>
    </tr>
    <tr>
      <td>Logs survive container restarts</td>
      <td>Pipeline (agents, buffers, backends) to operate</td>
    </tr>
    <tr>
      <td>Correlate with traces and metrics</td>
      <td>PII and secret leakage risk</td>
    </tr>
    <tr>
      <td>Supports incident response, forensics, compliance</td>
      <td>Noise drowns signal without discipline</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> logging as the primary monitoring tool. Teams build alerts on regexes over log text — fragile, slow, and expensive. Use metrics for alerting; use logs for investigation.</p>
</blockquote>

<ul>
  <li><strong>Unstructured, multi-line logs</strong> (stack traces split across events). Configure multiline parsing or log exceptions as single JSON fields.</li>
  <li><strong>Writing logs to files inside containers</strong> instead of stdout, so agents never see them.</li>
  <li><strong>No backpressure handling</strong> — a backend outage makes agents drop logs or exhaust node memory. Use buffers and limits.</li>
  <li><strong>Different field names per team</strong> (<code class="language-plaintext highlighter-rouge">traceId</code>, <code class="language-plaintext highlighter-rouge">trace_id</code>, <code class="language-plaintext highlighter-rouge">TraceID</code>) — cross-service queries break.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>Collectors</strong> — Fluent Bit, Fluentd, Vector, OpenTelemetry Collector, Logstash.</li>
  <li><strong>Backends</strong> — Elasticsearch/OpenSearch + Kibana/Dashboards, Grafana Loki, Splunk, Datadog, Elastic Cloud, cloud-native options (CloudWatch Logs, Azure Monitor Logs, Google Cloud Logging).</li>
  <li><strong>Buffers</strong> — Kafka between collectors and backends for high volume and resilience.</li>
</ul>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/correlation-id/">Correlation ID</a> — makes aggregated logs joinable.</li>
  <li><a href="/blog/distributed-tracing/">Distributed Tracing</a> — pivot from a log line to its trace.</li>
  <li><a href="/blog/application-metrics/">Application Metrics</a> — for counting and alerting.</li>
  <li><a href="/blog/audit-logging/">Audit Logging</a> — durable, tamper-evident records of user actions.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>Emit structured JSON logs to stdout with consistent fields, including trace and correlation IDs.</li>
  <li>Collect with node agents, enrich with platform metadata, redact, and route.</li>
  <li>Choose a backend by query needs and cost profile; keep index labels low-cardinality.</li>
  <li>Manage retention and noise deliberately — and use metrics, not logs, for alerting.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Observability" /><category term="log-aggregation" /><category term="structured-logging" /><category term="elk" /><category term="loki" /><category term="fluent-bit" /><category term="observability" /><summary type="html"><![CDATA[Log Aggregation pattern explained: centralizes logs. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Distributed Tracing: Following One Request Across Every Service</title><link href="https://karthiksukumaran.com/blog/distributed-tracing/" rel="alternate" type="text/html" title="Distributed Tracing: Following One Request Across Every Service" /><published>2026-10-17T09:50:00+05:30</published><updated>2026-10-17T09:50:00+05:30</updated><id>https://karthiksukumaran.com/blog/distributed-tracing</id><content type="html" xml:base="https://karthiksukumaran.com/blog/distributed-tracing/"><![CDATA[<p>“Checkout is slow” is a hard problem when checkout calls eight services, two databases, a cache, and a payment provider. Metrics tell you <strong>that</strong> p99 latency doubled; logs tell you <strong>what</strong> each service said. <strong>Distributed tracing</strong> tells you <strong>where the time went and where the error started</strong> — by recording the full path of a single request as a tree of timed operations.</p>

<h2 id="the-problem">The problem</h2>

<p>In a distributed system:</p>

<ul>
  <li>latency is the <em>sum and overlap</em> of many calls — which one is slow?</li>
  <li>errors surface far from their cause — a 500 at the gateway may originate in an inventory query three hops away;</li>
  <li>asynchronous hops (queues, topics) break the obvious request/response chain;</li>
  <li>each team sees only its own service’s logs and metrics.</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<p>A <strong>trace</strong> represents one end-to-end operation. It’s composed of <strong>spans</strong> — named, timed operations with attributes, events, and status — linked by parent/child relationships.</p>

<pre><code class="language-mermaid">gantt
    title Trace 4bf92f35… — POST /checkout (612 ms)
    dateFormat  x
    axisFormat  %L ms
    section Gateway
    POST /checkout                 :0, 612
    section Checkout svc
    PlaceOrder                     :8, 600
    validate cart                  :10, 40
    section Pricing svc
    GET /quote                     :42, 120
    section Inventory svc
    POST /reservations             :125, 455
    SELECT stock FOR UPDATE (slow) :130, 440
    section Payments svc
    POST /payments                 :458, 590
</code></pre>

<p><em>Reading the waterfall: the inventory <code class="language-plaintext highlighter-rouge">SELECT … FOR UPDATE</code> dominates the request — a lock-contention problem, not a CPU one.</em></p>

<p>Key concepts:</p>

<table>
  <thead>
    <tr>
      <th>Concept</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Trace ID</strong></td>
      <td>Shared by every span in the trace (the natural <a href="/blog/correlation-id/">Correlation ID</a>)</td>
    </tr>
    <tr>
      <td><strong>Span</strong></td>
      <td>One operation: name, start/end time, attributes (<code class="language-plaintext highlighter-rouge">http.route</code>, <code class="language-plaintext highlighter-rouge">db.system</code>), status, events</td>
    </tr>
    <tr>
      <td><strong>Parent span ID</strong></td>
      <td>Links a span to the span that caused it</td>
    </tr>
    <tr>
      <td><strong>Span kind</strong></td>
      <td><code class="language-plaintext highlighter-rouge">SERVER</code>, <code class="language-plaintext highlighter-rouge">CLIENT</code>, <code class="language-plaintext highlighter-rouge">PRODUCER</code>, <code class="language-plaintext highlighter-rouge">CONSUMER</code>, <code class="language-plaintext highlighter-rouge">INTERNAL</code></td>
    </tr>
    <tr>
      <td><strong>Context propagation</strong></td>
      <td>Passing trace ID + parent span ID across process boundaries (W3C <code class="language-plaintext highlighter-rouge">traceparent</code> header)</td>
    </tr>
    <tr>
      <td><strong>Span links</strong></td>
      <td>Connect spans that are related but not parent/child (batch consumers, fan-in)</td>
    </tr>
  </tbody>
</table>

<h2 id="opentelemetry-the-standard">OpenTelemetry: the standard</h2>

<p><strong>OpenTelemetry (OTel)</strong> — a CNCF project — provides vendor-neutral APIs, SDKs, auto-instrumentation, a wire protocol (OTLP), and the <strong>Collector</strong>. Instrument once; send to Jaeger, Tempo, X-Ray, Honeycomb, Datadog, or anything else.</p>

<pre><code class="language-mermaid">flowchart TD
    A[Service A&lt;br/&gt;OTel SDK / agent] --&gt;|OTLP| COL
    B[Service B&lt;br/&gt;OTel SDK / agent] --&gt;|OTLP| COL
    M[Mesh proxies] --&gt;|OTLP / Zipkin| COL
    subgraph COL["OpenTelemetry Collector"]
      R[receivers] --&gt; P[processors&lt;br/&gt;batch, tail sampling,&lt;br/&gt;attribute scrubbing] --&gt; E[exporters]
    end
    E --&gt; J[(Jaeger / Tempo)]
    E --&gt; V[(Vendor backend)]
</code></pre>

<h3 id="auto-instrumentation-java">Auto-instrumentation (Java)</h3>

<p>The Java agent instruments common frameworks (Spring, JDBC, Kafka clients, HTTP clients) with no code changes:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java <span class="nt">-javaagent</span>:/otel/opentelemetry-javaagent.jar <span class="se">\</span>
     <span class="nt">-Dotel</span>.service.name<span class="o">=</span>checkout <span class="se">\</span>
     <span class="nt">-Dotel</span>.resource.attributes<span class="o">=</span>deployment.environment<span class="o">=</span>prod,service.version<span class="o">=</span>2026.10.17 <span class="se">\</span>
     <span class="nt">-Dotel</span>.exporter.otlp.endpoint<span class="o">=</span>http://otel-collector:4317 <span class="se">\</span>
     <span class="nt">-Dotel</span>.traces.sampler<span class="o">=</span>parentbased_traceidratio <span class="se">\</span>
     <span class="nt">-Dotel</span>.traces.sampler.arg<span class="o">=</span>0.10 <span class="se">\</span>
     <span class="nt">-jar</span> checkout.jar
</code></pre></div></div>

<h3 id="manual-spans-for-business-operations">Manual spans for business operations</h3>

<p>Auto-instrumentation covers I/O; add spans and attributes for <strong>business meaning</strong>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">trace</span><span class="p">,</span> <span class="nx">SpanStatusCode</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@opentelemetry/api</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">tracer</span> <span class="o">=</span> <span class="nx">trace</span><span class="p">.</span><span class="nx">getTracer</span><span class="p">(</span><span class="dl">"</span><span class="s2">checkout</span><span class="dl">"</span><span class="p">);</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">placeOrder</span><span class="p">(</span><span class="nx">cart</span><span class="p">:</span> <span class="nx">Cart</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">tracer</span><span class="p">.</span><span class="nx">startActiveSpan</span><span class="p">(</span><span class="dl">"</span><span class="s2">PlaceOrder</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">span</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">span</span><span class="p">.</span><span class="nx">setAttributes</span><span class="p">({</span>
      <span class="dl">"</span><span class="s2">order.line_count</span><span class="dl">"</span><span class="p">:</span> <span class="nx">cart</span><span class="p">.</span><span class="nx">lines</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">order.total</span><span class="dl">"</span><span class="p">:</span> <span class="nx">cart</span><span class="p">.</span><span class="nx">total</span><span class="p">,</span>            <span class="c1">// business context, no PII</span>
      <span class="dl">"</span><span class="s2">customer.tier</span><span class="dl">"</span><span class="p">:</span> <span class="nx">cart</span><span class="p">.</span><span class="nx">customerTier</span><span class="p">,</span>
    <span class="p">});</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">quote</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">pricing</span><span class="p">.</span><span class="nx">quote</span><span class="p">(</span><span class="nx">cart</span><span class="p">);</span>        <span class="c1">// child CLIENT span via auto-instrumentation</span>
      <span class="kd">const</span> <span class="nx">reservation</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">inventory</span><span class="p">.</span><span class="nx">reserve</span><span class="p">(</span><span class="nx">cart</span><span class="p">);</span>
      <span class="nx">span</span><span class="p">.</span><span class="nx">addEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">stock.reserved</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">reservation.id</span><span class="dl">"</span><span class="p">:</span> <span class="nx">reservation</span><span class="p">.</span><span class="nx">id</span> <span class="p">});</span>
      <span class="k">return</span> <span class="k">await</span> <span class="nx">payments</span><span class="p">.</span><span class="nx">capture</span><span class="p">(</span><span class="nx">cart</span><span class="p">,</span> <span class="nx">quote</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">span</span><span class="p">.</span><span class="nx">recordException</span><span class="p">(</span><span class="nx">err</span> <span class="k">as</span> <span class="nb">Error</span><span class="p">);</span>
      <span class="nx">span</span><span class="p">.</span><span class="nx">setStatus</span><span class="p">({</span> <span class="na">code</span><span class="p">:</span> <span class="nx">SpanStatusCode</span><span class="p">.</span><span class="nx">ERROR</span><span class="p">,</span> <span class="na">message</span><span class="p">:</span> <span class="p">(</span><span class="nx">err</span> <span class="k">as</span> <span class="nb">Error</span><span class="p">).</span><span class="nx">message</span> <span class="p">});</span>
      <span class="k">throw</span> <span class="nx">err</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
      <span class="nx">span</span><span class="p">.</span><span class="nx">end</span><span class="p">();</span>
    <span class="p">}</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="tracing-across-messaging">Tracing across messaging</h2>

<p>Asynchronous hops need explicit propagation in message headers. OTel’s Kafka, RabbitMQ, and SQS instrumentations inject <code class="language-plaintext highlighter-rouge">traceparent</code> on produce and extract it on consume, creating <code class="language-plaintext highlighter-rouge">PRODUCER</code> and <code class="language-plaintext highlighter-rouge">CONSUMER</code> spans.</p>

<ul>
  <li>For <strong>one message → one handler</strong>, the consumer span is a child of the producer span.</li>
  <li>For <strong>batch consumers</strong>, use <strong>span links</strong> to each message’s context rather than a single parent.</li>
  <li>With an <strong>outbox</strong>, store the trace context in the outbox row so the relay’s publish span links back to the original request (<a href="/blog/transactional-outbox/">Transactional Outbox</a>).</li>
</ul>

<h2 id="sampling">Sampling</h2>

<p>Tracing every request at high volume is expensive. Choose a sampling strategy:</p>

<table>
  <thead>
    <tr>
      <th>Strategy</th>
      <th>How</th>
      <th>Pros</th>
      <th>Cons</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Head-based (probabilistic)</strong></td>
      <td>Decide at trace start (e.g., 10%); downstream follows the parent’s decision</td>
      <td>Cheap, simple</td>
      <td>May miss rare errors and slow outliers</td>
    </tr>
    <tr>
      <td><strong>Tail-based</strong></td>
      <td>Collector buffers complete traces, then keeps errors, slow traces, and a sample of the rest</td>
      <td>Keeps the interesting traces</td>
      <td>Collector memory/CPU; needs all spans of a trace at one collector</td>
    </tr>
    <tr>
      <td><strong>Rate-limited / adaptive</strong></td>
      <td>Cap traces per second per service/route</td>
      <td>Predictable cost</td>
      <td>Low-traffic routes may be under-sampled</td>
    </tr>
  </tbody>
</table>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># OpenTelemetry Collector: tail sampling — keep all errors and slow traces, 5% of the rest</span>
<span class="na">processors</span><span class="pi">:</span>
  <span class="na">tail_sampling</span><span class="pi">:</span>
    <span class="na">decision_wait</span><span class="pi">:</span> <span class="s">10s</span>
    <span class="na">policies</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">errors</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">status_code</span>
        <span class="na">status_code</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">status_codes</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">ERROR</span><span class="pi">]</span> <span class="pi">}</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">slow</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">latency</span>
        <span class="na">latency</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">threshold_ms</span><span class="pi">:</span> <span class="nv">1000</span> <span class="pi">}</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">baseline</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">probabilistic</span>
        <span class="na">probabilistic</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">sampling_percentage</span><span class="pi">:</span> <span class="nv">5</span> <span class="pi">}</span>
  <span class="na">attributes/scrub</span><span class="pi">:</span>
    <span class="na">actions</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">key</span><span class="pi">:</span> <span class="nv">http.request.header.authorization</span><span class="pi">,</span> <span class="nv">action</span><span class="pi">:</span> <span class="nv">delete</span> <span class="pi">}</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">key</span><span class="pi">:</span> <span class="nv">user.email</span><span class="pi">,</span> <span class="nv">action</span><span class="pi">:</span> <span class="nv">delete</span> <span class="pi">}</span>
</code></pre></div></div>

<h2 id="using-traces-well">Using traces well</h2>

<ul>
  <li><strong>Start from symptoms</strong> — jump from a slow-latency alert or error log line to exemplar traces (metrics with trace exemplars, logs with <code class="language-plaintext highlighter-rouge">trace_id</code>).</li>
  <li><strong>Look for patterns</strong>, not just single traces: sequential calls that could be parallel, N+1 queries, retries, lock waits.</li>
  <li><strong>Correlate the three signals</strong> — traces, metrics, logs — via trace IDs and shared resource attributes (<code class="language-plaintext highlighter-rouge">service.name</code>, <code class="language-plaintext highlighter-rouge">deployment.environment</code>).</li>
  <li><strong>Service graphs</strong> derived from spans show real dependencies — often surprising ones.</li>
</ul>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pinpoints latency and error sources across services</td>
      <td>Instrumentation effort, especially for async and legacy code</td>
    </tr>
    <tr>
      <td>Reveals actual dependencies and call patterns</td>
      <td>Storage and processing cost; sampling trade-offs</td>
    </tr>
    <tr>
      <td>Vendor-neutral with OpenTelemetry</td>
      <td>Context propagation gaps break traces</td>
    </tr>
    <tr>
      <td>Links metrics and logs to concrete requests</td>
      <td>Risk of leaking sensitive data in attributes</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> broken context. One service uses an HTTP client that isn’t instrumented, or a thread pool drops context, and the trace splits into disconnected fragments. Standardize on OTel auto-instrumentation and test propagation end-to-end in CI.</p>
</blockquote>

<ul>
  <li><strong>Head sampling at 1%</strong> and then wondering why the failing request has no trace — use tail sampling for errors.</li>
  <li><strong>High-cardinality span names</strong> (<code class="language-plaintext highlighter-rouge">GET /orders/8812</code>) instead of route templates (<code class="language-plaintext highlighter-rouge">GET /orders/{id}</code>).</li>
  <li><strong>PII in attributes</strong> — scrub at the SDK or Collector.</li>
  <li><strong>Tracing only the backend</strong> — include gateways, meshes, and (where useful) the browser/mobile client.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>OpenTelemetry</strong> SDKs, auto-instrumentation agents, and the Collector.</li>
  <li><strong>Backends</strong> — Jaeger, Zipkin, Grafana Tempo (open source); AWS X-Ray, Google Cloud Trace, Azure Monitor; Honeycomb, Datadog, New Relic, Dynatrace, Elastic APM.</li>
  <li><strong>Meshes</strong> (Istio, Linkerd) emit spans for every hop; application spans connect them internally.</li>
</ul>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/correlation-id/">Correlation ID</a> — the trace ID doubles as one.</li>
  <li><a href="/blog/log-aggregation/">Log Aggregation</a> — logs stamped with trace IDs.</li>
  <li><a href="/blog/application-metrics/">Application Metrics</a> — metrics with exemplars that link to traces.</li>
  <li><a href="/blog/service-mesh/">Service Mesh</a> — automatic spans at the network layer.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>A trace is a tree of timed spans for one request, stitched together by propagated context.</li>
  <li>Use OpenTelemetry: auto-instrument I/O, add business spans and attributes.</li>
  <li>Propagate context through messages, outboxes, and thread pools.</li>
  <li>Use tail sampling to keep errors and slow traces, and scrub sensitive data.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Observability" /><category term="distributed-tracing" /><category term="opentelemetry" /><category term="spans" /><category term="sampling" /><category term="jaeger" /><category term="observability" /><summary type="html"><![CDATA[Distributed Tracing pattern explained: follows a request across services. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Service Discovery: How Services Find Each Other</title><link href="https://karthiksukumaran.com/blog/service-discovery/" rel="alternate" type="text/html" title="Service Discovery: How Services Find Each Other" /><published>2026-10-17T09:49:00+05:30</published><updated>2026-10-17T09:49:00+05:30</updated><id>https://karthiksukumaran.com/blog/service-discovery</id><content type="html" xml:base="https://karthiksukumaran.com/blog/service-discovery/"><![CDATA[<p>In a static data center, the payments service lived at <code class="language-plaintext highlighter-rouge">10.4.2.17:8080</code> for years, written into a config file. In a cloud or Kubernetes environment, instances are created and destroyed constantly — autoscaling, deployments, node failures, spot interruptions — and each gets a new address. <strong>Service Discovery</strong> lets callers find the <strong>current, healthy instances</strong> of a service <strong>by name</strong>, at runtime.</p>

<h2 id="the-problem">The problem</h2>

<ul>
  <li>Instance addresses change with every deploy and scale event.</li>
  <li>Some instances are unhealthy, starting up, or draining.</li>
  <li>Hard-coded addresses or manually updated load-balancer pools break, lag, or route to dead instances.</li>
  <li>Different environments (dev, staging, prod, regions) need different addresses for the same logical service.</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<p>Every discovery system has a <strong>service registry</strong> — a database of <code class="language-plaintext highlighter-rouge">service name → healthy instances</code> — plus a way for instances to <strong>register</strong> and for callers to <strong>look up</strong>.</p>

<h3 id="registration">Registration</h3>

<table>
  <thead>
    <tr>
      <th>Style</th>
      <th>How</th>
      <th>Examples</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Self-registration</strong></td>
      <td>The instance registers itself on start, heartbeats, and deregisters on shutdown</td>
      <td>Eureka client, Consul agent API</td>
    </tr>
    <tr>
      <td><strong>Third-party registration</strong></td>
      <td>The platform registers instances it manages, based on health</td>
      <td>Kubernetes Endpoints/EndpointSlices, AWS ECS + Cloud Map, Consul catalog sync</td>
    </tr>
  </tbody>
</table>

<p>Third-party registration is generally more reliable: the platform knows when it starts and stops instances, and services don’t need registry code.</p>

<h3 id="lookup">Lookup</h3>

<pre><code class="language-mermaid">flowchart TD
    subgraph CS["Client-side discovery"]
      C1[Caller with discovery client] --&gt;|1 · query 'payments'| R1[(Registry)]
      R1 --&gt;|2 · instances list| C1
      C1 --&gt;|3 · pick one, call directly| I1[payments instance]
    end
    subgraph SS["Server-side discovery"]
      C2[Caller] --&gt;|1 · call payments.svc| LB[Load balancer / proxy]
      LB --&gt;|2 · looks up| R2[(Registry)]
      LB --&gt;|3 · forwards| I2[payments instance]
    end
</code></pre>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Client-side discovery</th>
      <th>Server-side discovery</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Who picks the instance</td>
      <td>The caller (library)</td>
      <td>A load balancer or proxy</td>
    </tr>
    <tr>
      <td>Load-balancing smarts</td>
      <td>Rich (zone-aware, least-request) in the client</td>
      <td>In the proxy</td>
    </tr>
    <tr>
      <td>Language coupling</td>
      <td>Needs a discovery client per language</td>
      <td>Language-agnostic</td>
    </tr>
    <tr>
      <td>Extra hop</td>
      <td>No</td>
      <td>Yes (unless the proxy is a local sidecar)</td>
    </tr>
    <tr>
      <td>Examples</td>
      <td>Eureka + Spring Cloud LoadBalancer, gRPC xDS clients</td>
      <td>Kubernetes Services (kube-proxy), AWS ALB, Envoy/mesh</td>
    </tr>
  </tbody>
</table>

<p>Service meshes blur the line: a <strong>local sidecar proxy</strong> does client-side-style load balancing on behalf of the app, configured via the mesh’s discovery (Envoy’s xDS APIs).</p>

<h2 id="kubernetes-discovery-built-in">Kubernetes: discovery built in</h2>

<p>In Kubernetes, discovery is a platform feature:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">payments</span><span class="pi">,</span> <span class="nv">namespace</span><span class="pi">:</span> <span class="nv">shop</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">app</span><span class="pi">:</span> <span class="nv">payments</span> <span class="pi">}</span>       <span class="c1"># pods with this label are endpoints</span>
  <span class="na">ports</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">http</span><span class="pi">,</span> <span class="nv">port</span><span class="pi">:</span> <span class="nv">80</span><span class="pi">,</span> <span class="nv">targetPort</span><span class="pi">:</span> <span class="nv">8080</span> <span class="pi">}]</span>
<span class="nn">---</span>
<span class="c1"># Only pods passing their readinessProbe appear in the EndpointSlice.</span>
<span class="c1"># Callers simply use DNS:</span>
<span class="c1">#   http://payments                       (same namespace)</span>
<span class="c1">#   http://payments.shop.svc.cluster.local (fully qualified)</span>
</code></pre></div></div>

<ul>
  <li><strong>Endpoints/EndpointSlices</strong> are maintained by the control plane from pod labels and <strong>readiness</strong> (<a href="/blog/health-check/">Health Check</a>).</li>
  <li><strong>CoreDNS</strong> resolves service names to a stable <strong>ClusterIP</strong>; kube-proxy (or eBPF in Cilium) load-balances to endpoints.</li>
  <li><strong>Headless Services</strong> (<code class="language-plaintext highlighter-rouge">clusterIP: None</code>) return pod IPs directly — useful for client-side load balancing (e.g., gRPC) and stateful sets.</li>
</ul>

<h2 id="consul-discovery-across-platforms">Consul: discovery across platforms</h2>

<p>Consul spans VMs, Kubernetes, and multiple data centers, with health checks and DNS/HTTP interfaces.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Service definition registered with the local Consul agent</span>
<span class="nx">service</span> <span class="p">{</span>
  <span class="nx">name</span> <span class="p">=</span> <span class="s2">"payments"</span>
  <span class="nx">port</span> <span class="p">=</span> <span class="mi">8080</span>
  <span class="nx">tags</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"v2"</span><span class="p">,</span> <span class="s2">"primary"</span><span class="p">]</span>
  <span class="nx">meta</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">version</span> <span class="p">=</span> <span class="s2">"2026.10.17"</span> <span class="p">}</span>

  <span class="nx">check</span> <span class="p">{</span>
    <span class="nx">id</span>       <span class="p">=</span> <span class="s2">"payments-http"</span>
    <span class="nx">http</span>     <span class="p">=</span> <span class="s2">"http://localhost:8080/actuator/health/readiness"</span>
    <span class="nx">interval</span> <span class="p">=</span> <span class="s2">"10s"</span>
    <span class="nx">timeout</span>  <span class="p">=</span> <span class="s2">"2s"</span>
    <span class="nx">deregister_critical_service_after</span> <span class="p">=</span> <span class="s2">"1m"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># DNS interface: only healthy instances are returned</span>
dig @127.0.0.1 <span class="nt">-p</span> 8600 payments.service.consul SRV

<span class="c"># HTTP API with health filtering</span>
curl <span class="nt">-s</span> <span class="s2">"http://127.0.0.1:8500/v1/health/service/payments?passing=true"</span>
</code></pre></div></div>

<h2 id="eureka--spring-cloud-client-side-discovery">Eureka + Spring Cloud: client-side discovery</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Service registers itself and fetches the registry</span>
<span class="na">spring</span><span class="pi">:</span>
  <span class="na">application</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">checkout</span>
<span class="na">eureka</span><span class="pi">:</span>
  <span class="na">client</span><span class="pi">:</span>
    <span class="na">service-url</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">defaultZone</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://eureka-1:8761/eureka/,http://eureka-2:8761/eureka/"</span> <span class="pi">}</span>
    <span class="na">registry-fetch-interval-seconds</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">instance</span><span class="pi">:</span>
    <span class="na">prefer-ip-address</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">lease-renewal-interval-in-seconds</span><span class="pi">:</span> <span class="m">10</span>
    <span class="na">lease-expiration-duration-in-seconds</span><span class="pi">:</span> <span class="m">30</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="nd">@LoadBalanced</span>                       <span class="c1">// resolves "http://payments" via the registry, client-side</span>
<span class="nc">RestClient</span><span class="o">.</span><span class="na">Builder</span> <span class="nf">restClientBuilder</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">();</span> <span class="o">}</span>

<span class="c1">// Usage</span>
<span class="n">restClientBuilder</span><span class="o">.</span><span class="na">build</span><span class="o">().</span><span class="na">post</span><span class="o">().</span><span class="na">uri</span><span class="o">(</span><span class="s">"http://payments/v1/payments"</span><span class="o">).</span><span class="na">body</span><span class="o">(</span><span class="n">req</span><span class="o">).</span><span class="na">retrieve</span><span class="o">();</span>
</code></pre></div></div>

<h2 id="design-considerations">Design considerations</h2>

<ul>
  <li><strong>Health integration</strong> — the registry must reflect <em>readiness</em>, not just “process exists”. Stale entries route traffic to dead instances.</li>
  <li><strong>Caching and TTLs</strong> — clients cache lookups. DNS TTLs, JVM DNS caching (<code class="language-plaintext highlighter-rouge">networkaddress.cache.ttl</code>), and registry fetch intervals determine how fast callers notice changes. Long caches → traffic to terminated instances; very short → registry load.</li>
  <li><strong>Graceful deregistration</strong> — on shutdown, deregister/fail readiness <em>before</em> stopping, and let in-flight requests drain.</li>
  <li><strong>Registry availability</strong> — the registry is critical infrastructure; run it highly available. Good clients keep using their last-known instance list if the registry is briefly unreachable (Eureka’s self-preservation mode is an example).</li>
  <li><strong>Locality</strong> — prefer instances in the same zone/region to cut latency and cross-zone costs (topology-aware routing).</li>
  <li><strong>Environment naming</strong> — use namespaces, data centers, or tags rather than embedding environment names in service names.</li>
</ul>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>No hard-coded addresses; works with autoscaling</td>
      <td>Registry is critical infrastructure</td>
    </tr>
    <tr>
      <td>Routes only to healthy instances</td>
      <td>Staleness windows from caching and TTLs</td>
    </tr>
    <tr>
      <td>Enables zone-aware and smarter load balancing</td>
      <td>Client-side discovery couples you to libraries</td>
    </tr>
    <tr>
      <td>Foundation for meshes, gateways, and failover</td>
      <td>Cross-platform discovery (VMs + K8s) adds complexity</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> JVM DNS caching. Some JVM configurations cache successful DNS lookups indefinitely (notably when a security manager is installed), so a client keeps calling an IP that no longer exists. Set <code class="language-plaintext highlighter-rouge">networkaddress.cache.ttl</code> to a small value (e.g., 30–60 s) in environments where IPs change.</p>
</blockquote>

<ul>
  <li><strong>Long-lived connections</strong> (HTTP/2, gRPC) pinned to old instances — new instances get no traffic. Use client-side balancing with periodic re-resolution or connection max-age.</li>
  <li><strong>Self-registration without deregistration</strong> — crashed instances linger until their lease expires.</li>
  <li><strong>Discovery that ignores readiness</strong> — new pods receive traffic before they’re warm.</li>
  <li><strong>Hard-coding cluster-internal DNS names</strong> into clients outside the cluster.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>Kubernetes</strong> Services, EndpointSlices, CoreDNS; headless Services for client-side load balancing.</li>
  <li><strong>HashiCorp Consul</strong> — multi-platform registry with health checks, DNS, and service mesh.</li>
  <li><strong>Netflix Eureka</strong> with <strong>Spring Cloud</strong> — client-side discovery popular in Spring estates.</li>
  <li><strong>AWS Cloud Map</strong>, <strong>ECS Service Connect</strong>, <strong>Azure Container Apps</strong> built-in discovery.</li>
  <li><strong>etcd / ZooKeeper</strong> — underlying registries for many systems; <strong>Envoy xDS</strong> — dynamic endpoint discovery for proxies.</li>
</ul>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/health-check/">Health Check</a> — determines which instances are discoverable.</li>
  <li><a href="/blog/service-mesh/">Service Mesh</a> — builds on discovery to route and secure traffic.</li>
  <li><a href="/blog/api-gateway/">API Gateway</a> — uses discovery to reach upstreams.</li>
  <li><a href="/blog/sidecar/">Sidecar</a> — local proxies that perform discovery-driven load balancing.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>A registry maps service names to healthy instances; instances register themselves or are registered by the platform.</li>
  <li>Client-side discovery gives smarter balancing; server-side is language-agnostic.</li>
  <li>On Kubernetes, use Services and DNS — readiness controls membership.</li>
  <li>Watch caching, long-lived connections, and graceful deregistration.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Integration" /><category term="service-discovery" /><category term="registry" /><category term="kubernetes-dns" /><category term="consul" /><category term="eureka" /><category term="load-balancing" /><summary type="html"><![CDATA[Service Discovery pattern explained: services find each other dynamically. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Service Mesh: Managing Service-to-Service Traffic as Infrastructure</title><link href="https://karthiksukumaran.com/blog/service-mesh/" rel="alternate" type="text/html" title="Service Mesh: Managing Service-to-Service Traffic as Infrastructure" /><published>2026-10-17T09:48:00+05:30</published><updated>2026-10-17T09:48:00+05:30</updated><id>https://karthiksukumaran.com/blog/service-mesh</id><content type="html" xml:base="https://karthiksukumaran.com/blog/service-mesh/"><![CDATA[<p>Once you have dozens of services calling each other, the <strong>network between them</strong> becomes a system in its own right. Who is allowed to call whom? Is that traffic encrypted? What’s the error rate between checkout and payments? How do you send 5% of traffic to a new version? A <strong>Service Mesh</strong> answers these questions as <strong>infrastructure</strong>: a fleet of proxies that handle every service-to-service request, configured centrally by a <strong>control plane</strong>.</p>

<h2 id="the-problem">The problem</h2>

<p>Without a mesh, each service team must implement — in each language — mutual TLS, retries, timeouts, circuit breaking, traffic splitting, and per-call metrics. In practice:</p>

<ul>
  <li>internal traffic is often <strong>unencrypted</strong> and <strong>unauthenticated</strong> (“it’s inside the cluster”);</li>
  <li>resilience policies are <strong>inconsistent</strong> across teams;</li>
  <li>there’s no uniform view of <strong>service-to-service health</strong>;</li>
  <li>canary releases and traffic shifting require custom code or load-balancer tickets.</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<pre><code class="language-mermaid">flowchart TD
    subgraph CP["Control plane (istiod / linkerd control plane)"]
      CA[Certificate authority&lt;br/&gt;workload identities]
      CFG[Config &amp; policy&lt;br/&gt;routes, retries, authz]
      DS[Service discovery]
    end
    subgraph P1["Pod: checkout"]
      A1[checkout app] --&gt; X1[proxy]
    end
    subgraph P2["Pod: payments"]
      X2[proxy] --&gt; A2[payments app]
    end
    X1 == "mTLS (SPIFFE identities)" ==&gt; X2
    CP -. "xDS config + certs" .-&gt; X1
    CP -. "xDS config + certs" .-&gt; X2
    X1 -. "metrics &amp; traces" .-&gt; OBS[(Prometheus / tracing backend)]
    X2 -. "metrics &amp; traces" .-&gt; OBS
</code></pre>

<ul>
  <li><strong>Data plane</strong> — proxies (Envoy in Istio, linkerd2-proxy in Linkerd) that intercept all inbound and outbound traffic of each workload, as <a href="/blog/sidecar/">sidecars</a> or as node-level/ambient components.</li>
  <li><strong>Control plane</strong> — distributes configuration and certificates to proxies, and watches the platform’s service registry.</li>
</ul>

<p>The application keeps making plain HTTP/gRPC calls; the mesh transparently adds security, policy, and telemetry.</p>

<h2 id="what-a-mesh-provides">What a mesh provides</h2>

<h3 id="1-security-mtls-and-identity">1. Security: mTLS and identity</h3>

<p>Each workload gets a cryptographic identity (typically a SPIFFE ID derived from its Kubernetes service account) with automatically rotated certificates. Traffic between proxies is <strong>mutually authenticated and encrypted</strong>, and you can write <strong>authorization policies by identity</strong>, not IP address.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Istio: require mTLS for every workload in the namespace</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">security.istio.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PeerAuthentication</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">default</span><span class="pi">,</span> <span class="nv">namespace</span><span class="pi">:</span> <span class="nv">payments</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">mtls</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">mode</span><span class="pi">:</span> <span class="nv">STRICT</span> <span class="pi">}</span>
<span class="nn">---</span>
<span class="c1"># Only the checkout service account may call payments' capture endpoint</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">security.istio.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">AuthorizationPolicy</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">payments-capture</span><span class="pi">,</span> <span class="nv">namespace</span><span class="pi">:</span> <span class="nv">payments</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">matchLabels</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">app</span><span class="pi">:</span> <span class="nv">payments</span> <span class="pi">}</span> <span class="pi">}</span>
  <span class="na">action</span><span class="pi">:</span> <span class="s">ALLOW</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">from</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">source</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">principals</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">cluster.local/ns/checkout/sa/checkout"</span><span class="pi">]</span> <span class="pi">}</span>
      <span class="na">to</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">operation</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">methods</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">POST"</span><span class="pi">],</span> <span class="nv">paths</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">/v1/payments/*/capture"</span><span class="pi">]</span> <span class="pi">}</span>
</code></pre></div></div>

<h3 id="2-traffic-management">2. Traffic management</h3>

<p>Weighted routing, header-based routing, mirroring, and fault injection — the tools for canaries, blue/green, and <a href="/blog/strangler-fig/">Strangler Fig</a> migrations.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.istio.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">VirtualService</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">payments</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">payments</span><span class="pi">]</span>
  <span class="na">http</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">match</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">headers</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">x-canary</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">exact</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span> <span class="pi">}</span> <span class="pi">}</span> <span class="pi">}]</span>
      <span class="na">route</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">destination</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">host</span><span class="pi">:</span> <span class="nv">payments</span><span class="pi">,</span> <span class="nv">subset</span><span class="pi">:</span> <span class="nv">v2</span> <span class="pi">}</span> <span class="pi">}]</span>
    <span class="pi">-</span> <span class="na">route</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="pi">{</span> <span class="nv">destination</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">host</span><span class="pi">:</span> <span class="nv">payments</span><span class="pi">,</span> <span class="nv">subset</span><span class="pi">:</span> <span class="nv">v1</span> <span class="pi">},</span> <span class="nv">weight</span><span class="pi">:</span> <span class="nv">95</span> <span class="pi">}</span>
        <span class="pi">-</span> <span class="pi">{</span> <span class="nv">destination</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">host</span><span class="pi">:</span> <span class="nv">payments</span><span class="pi">,</span> <span class="nv">subset</span><span class="pi">:</span> <span class="nv">v2</span> <span class="pi">},</span> <span class="nv">weight</span><span class="pi">:</span> <span class="nv">5</span> <span class="pi">}</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">2s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">attempts</span><span class="pi">:</span> <span class="nv">2</span><span class="pi">,</span> <span class="nv">perTryTimeout</span><span class="pi">:</span> <span class="nv">800ms</span><span class="pi">,</span> <span class="nv">retryOn</span><span class="pi">:</span> <span class="s2">"</span><span class="s">connect-failure,refused-stream,unavailable"</span> <span class="pi">}</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.istio.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">DestinationRule</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">payments</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">host</span><span class="pi">:</span> <span class="s">payments</span>
  <span class="na">subsets</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">v1</span><span class="pi">,</span> <span class="nv">labels</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">version</span><span class="pi">:</span> <span class="nv">v1</span> <span class="pi">}</span> <span class="pi">}</span>
    <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">v2</span><span class="pi">,</span> <span class="nv">labels</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">version</span><span class="pi">:</span> <span class="nv">v2</span> <span class="pi">}</span> <span class="pi">}</span>
  <span class="na">trafficPolicy</span><span class="pi">:</span>
    <span class="na">outlierDetection</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">consecutive5xxErrors</span><span class="pi">:</span> <span class="nv">5</span><span class="pi">,</span> <span class="nv">interval</span><span class="pi">:</span> <span class="nv">10s</span><span class="pi">,</span> <span class="nv">baseEjectionTime</span><span class="pi">:</span> <span class="nv">30s</span> <span class="pi">}</span>
</code></pre></div></div>

<h3 id="3-resilience">3. Resilience</h3>

<p>Timeouts, retries with budgets, connection-pool limits (<a href="/blog/bulkhead/">Bulkhead</a>), and outlier ejection (a form of <a href="/blog/circuit-breaker/">Circuit Breaker</a>) — configured consistently, per destination.</p>

<h3 id="4-observability">4. Observability</h3>

<p>Every proxy emits <strong>golden signals</strong> — request rate, error rate, latency — per source and destination, plus access logs and trace spans. You get a service graph without instrumenting code (though application-level tracing is still needed to connect spans <em>inside</em> a service; see <a href="/blog/distributed-tracing/">Distributed Tracing</a>).</p>

<h2 id="data-plane-models">Data-plane models</h2>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>How</th>
      <th>Pros</th>
      <th>Cons</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Sidecar</strong> (classic Istio, Linkerd)</td>
      <td>A proxy in every pod</td>
      <td>Mature; per-workload isolation; full L7</td>
      <td>CPU/memory per pod; injection and lifecycle complexity</td>
    </tr>
    <tr>
      <td><strong>Ambient / sidecar-less</strong> (Istio ambient: ztunnel + waypoints)</td>
      <td>Node-level L4 proxy for mTLS; optional per-namespace L7 waypoint proxies</td>
      <td>Much lower overhead; no app restarts to join</td>
      <td>Newer; L7 features need waypoints</td>
    </tr>
    <tr>
      <td><strong>eBPF-based</strong> (Cilium service mesh)</td>
      <td>Kernel-level L3/L4, Envoy for L7</td>
      <td>Very efficient networking</td>
      <td>L7 features still use proxies; tied to Cilium CNI</td>
    </tr>
  </tbody>
</table>

<h2 id="is-a-mesh-worth-it">Is a mesh worth it?</h2>

<p>A mesh is significant operational investment. Rough guidance:</p>

<table>
  <thead>
    <tr>
      <th>Signal</th>
      <th>Mesh helps</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Regulatory or zero-trust requirement for encrypted, authenticated internal traffic</td>
      <td>Strongly</td>
    </tr>
    <tr>
      <td>Many services in several languages needing consistent resilience and telemetry</td>
      <td>Strongly</td>
    </tr>
    <tr>
      <td>Frequent canaries / progressive delivery</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>A handful of services in one language with good libraries</td>
      <td>Probably not yet</td>
    </tr>
    <tr>
      <td>Team lacks Kubernetes networking experience</td>
      <td>Start with simpler tools; build skills first</td>
    </tr>
  </tbody>
</table>

<p>Linkerd’s design emphasizes simplicity and low overhead; Istio offers the broadest feature set; Consul spans Kubernetes and VMs. Many teams start with mTLS and telemetry only, then adopt traffic management later.</p>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Uniform mTLS and identity-based authorization</td>
      <td>Operational complexity: control plane, upgrades, certificates</td>
    </tr>
    <tr>
      <td>Consistent timeouts, retries, and outlier detection</td>
      <td>Resource overhead (especially with sidecars)</td>
    </tr>
    <tr>
      <td>Language-agnostic traffic control for canaries and migrations</td>
      <td>Added latency per hop (typically small, but nonzero)</td>
    </tr>
    <tr>
      <td>Golden-signal metrics for every service pair</td>
      <td>Debugging now includes the mesh layer</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> retries configured in the mesh <em>and</em> in application clients <em>and</em> at the gateway. A single failing call becomes dozens. Decide which layer owns retries for each path — see <a href="/blog/retry-with-backoff/">Retry with Backoff</a>.</p>
</blockquote>

<ul>
  <li><strong>Turning on <code class="language-plaintext highlighter-rouge">STRICT</code> mTLS cluster-wide on day one</strong> — break non-meshed clients; roll out with <code class="language-plaintext highlighter-rouge">PERMISSIVE</code> first and watch telemetry.</li>
  <li><strong>Treating the mesh as authorization for business rules</strong> — it knows <em>which service</em> calls; it doesn’t know <em>which customer</em> may refund <em>which order</em>.</li>
  <li><strong>Ignoring upgrades</strong> — control-plane and proxy versions must be upgraded regularly and carefully.</li>
  <li><strong>No owner</strong> — a mesh needs a platform team.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>Istio</strong> — feature-rich; sidecar and ambient modes; Envoy data plane.</li>
  <li><strong>Linkerd</strong> — lightweight, Rust-based proxy, focused on simplicity and security.</li>
  <li><strong>Consul service mesh</strong> — multi-platform (Kubernetes and VMs).</li>
  <li><strong>Cilium</strong> — eBPF networking with mesh capabilities; <strong>Kuma</strong> / <strong>Kong Mesh</strong> — Envoy-based, multi-zone.</li>
</ul>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/sidecar/">Sidecar</a> — the classic data-plane deployment.</li>
  <li><a href="/blog/service-discovery/">Service Discovery</a> — the mesh builds on the platform’s registry.</li>
  <li><a href="/blog/api-gateway/">API Gateway</a> — north-south counterpart to the mesh’s east-west traffic.</li>
  <li><a href="/blog/circuit-breaker/">Circuit Breaker</a>, <a href="/blog/timeout-pattern/">Timeout</a>, <a href="/blog/retry-with-backoff/">Retry with Backoff</a> — policies the mesh can enforce.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>A mesh = data-plane proxies handling all service traffic + a control plane configuring them.</li>
  <li>Its biggest wins are mTLS identity, consistent resilience policy, and uniform telemetry.</li>
  <li>Choose sidecar, ambient, or eBPF models based on overhead and maturity needs.</li>
  <li>Adopt incrementally, own it with a platform team, and avoid layering retries.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Integration" /><category term="service-mesh" /><category term="istio" /><category term="linkerd" /><category term="mtls" /><category term="traffic-management" /><category term="zero-trust" /><summary type="html"><![CDATA[Service Mesh pattern explained: manages service-to-service traffic. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Sidecar: A Helper Container Next to Every Service</title><link href="https://karthiksukumaran.com/blog/sidecar/" rel="alternate" type="text/html" title="Sidecar: A Helper Container Next to Every Service" /><published>2026-10-17T09:47:00+05:30</published><updated>2026-10-17T09:47:00+05:30</updated><id>https://karthiksukumaran.com/blog/sidecar</id><content type="html" xml:base="https://karthiksukumaran.com/blog/sidecar/"><![CDATA[<p>A motorcycle sidecar rides alongside the bike, shares its journey, and adds capacity without redesigning the motorcycle. The <strong>Sidecar</strong> pattern deploys a <strong>helper process next to each service instance</strong> — in the same Kubernetes pod, sharing its network namespace and lifecycle — to provide <strong>cross-cutting capabilities</strong> the service itself doesn’t implement.</p>

<p>Sidecars are how service meshes inject mTLS and traffic control, how many teams ship logs and metrics, and how runtimes like Dapr give every language the same building blocks.</p>

<h2 id="the-problem">The problem</h2>

<p>Every service needs the same plumbing: TLS, retries and timeouts, metrics and traces, log shipping, secret fetching, configuration reload, auth token validation. Implementing these in every service:</p>

<ul>
  <li><strong>duplicates effort</strong> across teams and languages,</li>
  <li><strong>drifts</strong> — each team’s version behaves slightly differently,</li>
  <li><strong>couples upgrades</strong> — patching a TLS library means rebuilding and redeploying every service,</li>
  <li>is <strong>hard for polyglot estates</strong> — the Java library doesn’t help the Python or Go teams.</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<pre><code class="language-mermaid">flowchart TD
    subgraph Pod["Kubernetes Pod (shared network namespace &amp; volumes)"]
      APP[Application container&lt;br/&gt;business logic only]
      PX[Proxy sidecar&lt;br/&gt;Envoy: mTLS, retries, metrics]
      LOG[Log shipper sidecar&lt;br/&gt;Fluent Bit]
      SEC[Secrets sidecar&lt;br/&gt;Vault Agent]
      APP -- "localhost:15001" --&gt; PX
      APP -- "writes /var/log/app" --&gt; LOG
      SEC -- "renders /vault/secrets/db" --&gt; APP
    end
    PX -- "mTLS" --&gt; OTHER[Other services]
    LOG --&gt; SINK[(Log platform)]
    SEC --&gt; VAULT[(Vault)]
</code></pre>

<p>Because sidecars share the pod’s <strong>network</strong> (localhost) and can share <strong>volumes</strong>, they can intercept traffic, read files the app writes, or provide files the app reads — without the application knowing much about them.</p>

<h2 id="common-sidecar-roles">Common sidecar roles</h2>

<table>
  <thead>
    <tr>
      <th>Role</th>
      <th>Example</th>
      <th>What the app gains</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Network proxy</strong></td>
      <td>Envoy (Istio), linkerd2-proxy</td>
      <td>mTLS, retries, timeouts, traffic splitting, L7 metrics</td>
    </tr>
    <tr>
      <td><strong>Distributed runtime</strong></td>
      <td>Dapr</td>
      <td>State, pub/sub, bindings, workflows, secrets via a local HTTP/gRPC API</td>
    </tr>
    <tr>
      <td><strong>Telemetry agent</strong></td>
      <td>OpenTelemetry Collector</td>
      <td>Local OTLP endpoint, batching, enrichment, export</td>
    </tr>
    <tr>
      <td><strong>Log shipping</strong></td>
      <td>Fluent Bit, Vector</td>
      <td>Tail files, parse, forward</td>
    </tr>
    <tr>
      <td><strong>Secrets / config</strong></td>
      <td>Vault Agent, CSI drivers, config reloaders</td>
      <td>Fetch and rotate credentials into files</td>
    </tr>
    <tr>
      <td><strong>Auth</strong></td>
      <td>OAuth2 Proxy, OPA</td>
      <td>Authentication and policy decisions at localhost</td>
    </tr>
    <tr>
      <td><strong>Adapter</strong></td>
      <td>Protocol translators, legacy format converters</td>
      <td>Expose a modern API for a legacy process</td>
    </tr>
  </tbody>
</table>

<h2 id="example-native-sidecars-in-kubernetes">Example: native sidecars in Kubernetes</h2>

<p>Kubernetes 1.29+ supports <strong>native sidecars</strong>: init containers with <code class="language-plaintext highlighter-rouge">restartPolicy: Always</code>. They start <strong>before</strong> the app, keep running alongside it, and shut down <strong>after</strong> it — fixing long-standing ordering problems (e.g., the app starting before the proxy is ready, or Jobs never completing because the sidecar keeps running).</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">statements</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">initContainers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">otel-collector</span>                  <span class="c1"># native sidecar</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">otel/opentelemetry-collector-contrib:0.110.0</span>
          <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>
          <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">--config=/conf/collector.yaml"</span><span class="pi">]</span>
          <span class="na">ports</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">containerPort</span><span class="pi">:</span> <span class="nv">4317</span> <span class="pi">}]</span>       <span class="c1"># app sends OTLP to localhost:4317</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">cpu</span><span class="pi">:</span> <span class="nv">50m</span><span class="pi">,</span> <span class="nv">memory</span><span class="pi">:</span> <span class="nv">64Mi</span> <span class="pi">}</span>
            <span class="na">limits</span><span class="pi">:</span>   <span class="pi">{</span> <span class="nv">memory</span><span class="pi">:</span> <span class="nv">128Mi</span> <span class="pi">}</span>
          <span class="na">volumeMounts</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">otel-conf</span><span class="pi">,</span> <span class="nv">mountPath</span><span class="pi">:</span> <span class="nv">/conf</span> <span class="pi">}]</span>

        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">log-shipper</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">fluent/fluent-bit:3.1</span>
          <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>
          <span class="na">volumeMounts</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">app-logs</span><span class="pi">,</span> <span class="nv">mountPath</span><span class="pi">:</span> <span class="nv">/var/log/app</span><span class="pi">,</span> <span class="nv">readOnly</span><span class="pi">:</span> <span class="nv">true</span> <span class="pi">}]</span>

      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">registry.example.com/statements:2026.10.17</span>
          <span class="na">env</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">OTEL_EXPORTER_OTLP_ENDPOINT</span><span class="pi">,</span> <span class="nv">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://localhost:4317"</span> <span class="pi">}</span>
          <span class="na">volumeMounts</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">app-logs</span><span class="pi">,</span> <span class="nv">mountPath</span><span class="pi">:</span> <span class="nv">/var/log/app</span> <span class="pi">}]</span>

      <span class="na">volumes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">app-logs</span><span class="pi">,</span> <span class="nv">emptyDir</span><span class="pi">:</span> <span class="pi">{}</span> <span class="pi">}</span>
        <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">otel-conf</span><span class="pi">,</span> <span class="nv">configMap</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">statements-otel</span> <span class="pi">}</span> <span class="pi">}</span>
</code></pre></div></div>

<h2 id="example-dapr-sidecar">Example: Dapr sidecar</h2>

<p>Dapr injects a <code class="language-plaintext highlighter-rouge">daprd</code> sidecar that exposes building blocks over localhost. The app publishes an event without a Kafka client library:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span>

<span class="c1"># Dapr sidecar listens on localhost:3500; "pubsub" component maps to Kafka, Service Bus, etc.
</span><span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span>
    <span class="s">"http://localhost:3500/v1.0/publish/pubsub/statements.generated"</span><span class="p">,</span>
    <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s">"statementId"</span><span class="p">:</span> <span class="s">"st-2026-10-0042"</span><span class="p">,</span> <span class="s">"accountId"</span><span class="p">:</span> <span class="s">"AC-1842"</span><span class="p">},</span>
    <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"traceparent"</span><span class="p">:</span> <span class="n">current_traceparent</span><span class="p">()},</span>
    <span class="n">timeout</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">dapr.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Component</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">pubsub</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">pubsub.kafka</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">v1</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">brokers</span><span class="pi">,</span> <span class="nv">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">kafka:9092"</span> <span class="pi">}</span>
    <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">consumerGroup</span><span class="pi">,</span> <span class="nv">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">statements"</span> <span class="pi">}</span>
    <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">authType</span><span class="pi">,</span> <span class="nv">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">none"</span> <span class="pi">}</span>   <span class="c1"># use mTLS/SASL in real environments</span>
</code></pre></div></div>

<p>Switching from Kafka to Azure Service Bus becomes a component change, not a code change.</p>

<h2 id="sidecar-vs-alternatives">Sidecar vs. alternatives</h2>

<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>Pros</th>
      <th>Cons</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>In-process library</strong></td>
      <td>Fastest; no extra containers</td>
      <td>Per-language; upgrades require redeploys</td>
    </tr>
    <tr>
      <td><strong>Sidecar per pod</strong></td>
      <td>Language-agnostic; upgrade independently; strong isolation</td>
      <td>CPU/memory per pod; extra hop; lifecycle complexity</td>
    </tr>
    <tr>
      <td><strong>Node agent (DaemonSet)</strong></td>
      <td>One per node; lower overhead</td>
      <td>Shared across tenants; less per-workload isolation</td>
    </tr>
    <tr>
      <td><strong>Sidecar-less mesh</strong> (Istio ambient, Cilium)</td>
      <td>No per-pod proxy for L4; optional L7 waypoints</td>
      <td>Newer; different operational model</td>
    </tr>
  </tbody>
</table>

<h2 id="design-considerations">Design considerations</h2>

<ul>
  <li><strong>Resource requests and limits</strong> for every sidecar — they count toward pod scheduling and can be OOM-killed.</li>
  <li><strong>Startup and shutdown ordering</strong> — use native sidecars, or hold the app’s start until the proxy is ready.</li>
  <li><strong>Version management</strong> — sidecars are often injected by a platform (mutating webhooks); coordinate upgrades and roll them like any release.</li>
  <li><strong>Security</strong> — sidecars share the pod’s network and often its service account; give them least privilege.</li>
  <li><strong>Observability of the sidecar itself</strong> — its health, latency, and errors are now part of your service’s behavior.</li>
</ul>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cross-cutting concerns without app code changes</td>
      <td>Extra CPU and memory in every pod</td>
    </tr>
    <tr>
      <td>Same capabilities for every language</td>
      <td>Additional network hop / latency (usually sub-millisecond on localhost)</td>
    </tr>
    <tr>
      <td>Independent upgrades of platform features</td>
      <td>More containers to debug; lifecycle subtleties</td>
    </tr>
    <tr>
      <td>Clear separation of business logic and plumbing</td>
      <td>Platform dependency and injection magic</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> Jobs that never finish. A classic (pre-native-sidecar) bug: a Kubernetes Job’s main container exits, but the proxy or log-shipper sidecar keeps running, so the Job never completes. Native sidecars (<code class="language-plaintext highlighter-rouge">restartPolicy: Always</code> init containers) fix this.</p>
</blockquote>

<ul>
  <li><strong>No resource limits</strong> — a misbehaving log shipper starves the application.</li>
  <li><strong>App starts before the proxy</strong> — the first requests fail with connection errors.</li>
  <li><strong>Too many sidecars</strong> — five helpers per pod multiplies resource overhead across thousands of pods.</li>
  <li><strong>Hidden coupling</strong> — the app depends on sidecar behavior (retries, auth) that isn’t documented or tested.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>Envoy</strong> (Istio, Consul), <strong>linkerd2-proxy</strong> — mesh data-plane sidecars.</li>
  <li><strong>Dapr</strong> — portable building blocks via sidecar.</li>
  <li><strong>OpenTelemetry Collector</strong>, <strong>Fluent Bit</strong>, <strong>Vector</strong> — telemetry and logs.</li>
  <li><strong>Vault Agent Injector</strong>, <strong>Secrets Store CSI Driver</strong> — secrets delivery.</li>
  <li><strong>OAuth2 Proxy</strong>, <strong>Open Policy Agent</strong> — authentication and authorization sidecars.</li>
</ul>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/service-mesh/">Service Mesh</a> — a fleet of proxy sidecars plus a control plane.</li>
  <li><a href="/blog/distributed-tracing/">Distributed Tracing</a> and <a href="/blog/log-aggregation/">Log Aggregation</a> — commonly delivered via sidecars.</li>
  <li><a href="/blog/anti-corruption-layer/">Anti-Corruption Layer</a> — an adapter sidecar can translate a legacy protocol.</li>
  <li><a href="/blog/health-check/">Health Check</a> — sidecar readiness affects pod readiness.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>A sidecar is a helper container sharing the service’s pod, network, and lifecycle.</li>
  <li>It delivers cross-cutting capabilities to any language without code changes.</li>
  <li>Budget resources, manage ordering (native sidecars), and secure each sidecar.</li>
  <li>Weigh it against libraries, node agents, and sidecar-less meshes.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Integration" /><category term="sidecar" /><category term="kubernetes" /><category term="envoy" /><category term="dapr" /><category term="cross-cutting-concerns" /><category term="polyglot" /><summary type="html"><![CDATA[Sidecar pattern explained: helper container next to a service. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Strangler Fig: Replacing a Monolith One Slice at a Time</title><link href="https://karthiksukumaran.com/blog/strangler-fig/" rel="alternate" type="text/html" title="Strangler Fig: Replacing a Monolith One Slice at a Time" /><published>2026-10-17T09:46:00+05:30</published><updated>2026-10-17T09:46:00+05:30</updated><id>https://karthiksukumaran.com/blog/strangler-fig</id><content type="html" xml:base="https://karthiksukumaran.com/blog/strangler-fig/"><![CDATA[<p>Strangler figs germinate in the branches of a host tree, send roots down around it, and grow until they can stand on their own — by which time the host has gone. Martin Fowler borrowed the image for the <strong>Strangler Fig</strong> pattern: build the new system <strong>around the edges</strong> of the old one, move functionality over <strong>one slice at a time</strong>, and retire the legacy system when nothing depends on it any more.</p>

<p>It’s the antidote to the big-bang rewrite — the multi-year project that tries to replace everything at once and too often fails to ship.</p>

<h2 id="the-problem">The problem</h2>

<p>Big-bang rewrites fail for predictable reasons:</p>

<ul>
  <li>The old system keeps changing while you rebuild it — you’re chasing a moving target.</li>
  <li>Hidden behavior (undocumented rules, batch jobs, edge cases) is discovered at cut-over.</li>
  <li>No value reaches users until the very end.</li>
  <li>Cut-over day is a single, enormous, high-risk event.</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<pre><code class="language-mermaid">flowchart TD
    U[Users / clients] --&gt; P[["Routing facade&lt;br/&gt;(gateway / proxy)"]]
    P --&gt;|"/accounts/statements → new"| N1[New Statements service]
    P --&gt;|"/accounts/profile → new"| N2[New Profile service]
    P --&gt;|"everything else → legacy"| L[Legacy monolith]
    N1 -. "ACL" .-&gt; L
    L -- "CDC / events" --&gt; N2
</code></pre>

<ol>
  <li><strong>Intercept</strong> — put a routing layer (API gateway, reverse proxy, or a facade in the monolith) in front of the legacy system. Initially it routes 100% to legacy.</li>
  <li><strong>Pick a slice</strong> — a capability with clear boundaries and real value.</li>
  <li><strong>Build it new</strong> — in the target architecture, behind an <a href="/blog/anti-corruption-layer/">Anti-Corruption Layer</a> where it must talk to legacy.</li>
  <li><strong>Divert traffic</strong> — route that slice to the new system, gradually (by user, percentage, or tenant).</li>
  <li><strong>Remove</strong> the legacy code for that slice once it’s no longer used.</li>
  <li><strong>Repeat</strong> until the legacy system is empty — then switch it off.</li>
</ol>

<h2 id="example-routing-with-nginx">Example: routing with NGINX</h2>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">upstream</span> <span class="s">legacy</span>  <span class="p">{</span> <span class="kn">server</span> <span class="nf">legacy-app.internal</span><span class="p">:</span><span class="mi">8080</span><span class="p">;</span> <span class="p">}</span>
<span class="k">upstream</span> <span class="s">stmts</span>   <span class="p">{</span> <span class="kn">server</span> <span class="nf">statements.svc.internal</span><span class="p">:</span><span class="mi">8080</span><span class="p">;</span> <span class="p">}</span>

<span class="c1"># Canary by cookie/header first, then by percentage</span>
<span class="k">split_clients</span> <span class="s">"</span>$<span class="p">{</span><span class="kn">remote_addr</span><span class="err">}</span>$<span class="p">{</span><span class="kn">http_user_agent</span><span class="err">}</span><span class="s">"</span> <span class="nv">$statements_backend</span> <span class="p">{</span>
    <span class="kn">20%</span>   <span class="s">stmts</span><span class="p">;</span>          <span class="c1"># 20% of anonymous traffic to the new service</span>
    <span class="kn">*</span>     <span class="s">legacy</span><span class="p">;</span>
<span class="p">}</span>

<span class="kn">map</span> <span class="nv">$http_x_canary</span> <span class="nv">$statements_target</span> <span class="p">{</span>
    <span class="kn">"true"</span>   <span class="s">stmts</span><span class="p">;</span>       <span class="c1"># internal testers always hit the new path</span>
    <span class="kn">default</span>  <span class="nv">$statements_backend</span><span class="p">;</span>
<span class="p">}</span>

<span class="kn">server</span> <span class="p">{</span>
  <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span>

  <span class="kn">location</span> <span class="n">/accounts/statements</span> <span class="p">{</span>
    <span class="kn">proxy_pass</span> <span class="s">http://</span><span class="nv">$statements_target</span><span class="p">;</span>
    <span class="kn">proxy_set_header</span> <span class="s">X-Correlation-ID</span> <span class="nv">$request_id</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
    <span class="kn">proxy_pass</span> <span class="s">http://legacy</span><span class="p">;</span>     <span class="c1"># everything not yet migrated</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Gateways (Kong, Envoy, the Kubernetes Gateway API) offer the same with weighted routes and header matches, and feature flags can route by customer segment inside the application.</p>

<h2 id="choosing-the-first-slice">Choosing the first slice</h2>

<table>
  <thead>
    <tr>
      <th>Good first slices</th>
      <th>Poor first slices</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Read-mostly features (statements, catalog browsing)</td>
      <td>The core transactional heart (ledger posting)</td>
    </tr>
    <tr>
      <td>Clear inputs/outputs and few dependencies</td>
      <td>Features tangled with dozens of tables</td>
    </tr>
    <tr>
      <td>High change rate (you’ll benefit immediately)</td>
      <td>Stable features nobody needs to change</td>
    </tr>
    <tr>
      <td>Visible value to users or the business</td>
      <td>Pure “technical” rewrites with no observable benefit</td>
    </tr>
    <tr>
      <td>New features that would otherwise be built in legacy</td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Rule of thumb:</strong> stop adding features to the legacy system first. Every <em>new</em> capability goes into the new architecture — this alone starts the strangling.</p>
</blockquote>

<h2 id="the-data-problem">The data problem</h2>

<p>Routing requests is the easy part; data is the hard part. Options, roughly in order of maturity:</p>

<pre><code class="language-mermaid">flowchart TD
    A["Phase 1 · New service reads legacy data&lt;br/&gt;via ACL / API (legacy is source of truth)"] --&gt; B["Phase 2 · New service keeps its own copy&lt;br/&gt;synced via CDC from legacy tables"]
    B --&gt; C["Phase 3 · New service becomes source of truth&lt;br/&gt;legacy reads from it (reverse sync)"]
    C --&gt; D["Phase 4 · Legacy dependency removed&lt;br/&gt;legacy tables archived"]
</code></pre>

<ul>
  <li><strong><a href="/blog/change-data-capture/">Change Data Capture</a></strong> from legacy tables keeps new stores in sync without modifying legacy code.</li>
  <li><strong>Dual writes are dangerous</strong> — prefer one source of truth per data set at each phase, with one-directional sync.</li>
  <li><strong>Reconcile</strong> — run automated comparisons between old and new data during migration and alert on drift.</li>
</ul>

<h2 id="reducing-risk-shadow-traffic-and-parallel-runs">Reducing risk: shadow traffic and parallel runs</h2>

<p>Before sending real users to the new slice, compare behaviors:</p>

<ul>
  <li><strong>Shadow (dark) traffic</strong> — mirror production requests to the new service, discard its responses, and compare them with legacy’s. Envoy/Istio support request mirroring.</li>
  <li><strong>Parallel run</strong> — for calculations (interest, fees, eligibility), run both and alert on differences; the legacy result remains authoritative until confidence is high.</li>
  <li><strong>Gradual rollout</strong> — internal users → 1% → 10% → 50% → 100%, with a one-switch rollback.</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Istio: mirror 100% of statement traffic to the new service while legacy still serves</span>
<span class="na">http</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">match</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">uri</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">prefix</span><span class="pi">:</span> <span class="nv">/accounts/statements</span> <span class="pi">}</span> <span class="pi">}]</span>
    <span class="na">route</span><span class="pi">:</span> <span class="pi">[{</span> <span class="nv">destination</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">host</span><span class="pi">:</span> <span class="nv">legacy</span> <span class="pi">}</span> <span class="pi">}]</span>
    <span class="na">mirror</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">host</span><span class="pi">:</span> <span class="nv">statements</span> <span class="pi">}</span>
    <span class="na">mirrorPercentage</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">value</span><span class="pi">:</span> <span class="nv">100.0</span> <span class="pi">}</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Warning:</strong> shadow traffic must not cause side effects. Mirrored <em>writes</em> to a new service that sends emails or posts to a ledger will double-act. Mirror reads freely; mirror writes only to sandboxed or no-op paths.</p>
</blockquote>

<h2 id="finishing-the-job">Finishing the job</h2>

<p>Strangler migrations often stall at 80%. The last 20% is the hardest code with the least glamour — and keeping two systems running indefinitely is the worst outcome. Keep momentum with:</p>

<ul>
  <li>A <strong>visible migration map</strong> of slices: legacy, in progress, migrated, removed.</li>
  <li><strong>Decommissioning as a goal</strong> with dates and budget, not an afterthought.</li>
  <li><strong>Deleting legacy code</strong> for each migrated slice (not just routing around it).</li>
  <li>Measuring <strong>legacy traffic</strong> and <strong>legacy change requests</strong> trending to zero.</li>
</ul>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Value delivered continuously, not at the end</td>
      <td>Two systems run in parallel for a long time</td>
    </tr>
    <tr>
      <td>Small, reversible steps reduce risk</td>
      <td>Routing and data-sync infrastructure to maintain</td>
    </tr>
    <tr>
      <td>Learn the legacy behavior slice by slice</td>
      <td>Temporary integration code (ACLs, CDC)</td>
    </tr>
    <tr>
      <td>The business keeps evolving during migration</td>
      <td>Risk of stalling before decommissioning</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> recreating the monolith’s structure in microservices — same data model, same boundaries, same coupling — just over HTTP. Use the migration to redraw boundaries around business capabilities and <a href="/blog/aggregate-ddd/">aggregates</a>.</p>
</blockquote>

<ul>
  <li><strong>No routing layer</strong> — every client must be changed to call the new service.</li>
  <li><strong>Bidirectional sync</strong> between old and new databases, causing loops and conflicts.</li>
  <li><strong>Migrating the easiest technical layer first</strong> (e.g., “the UI”) with no capability moving.</li>
  <li><strong>Never deleting legacy code</strong>, so the old system keeps being maintained.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>Routing</strong> — API gateways (Kong, Apigee, AWS API Gateway), reverse proxies (NGINX, Envoy, HAProxy), service meshes (traffic splitting and mirroring), feature-flag platforms.</li>
  <li><strong>Data</strong> — Debezium, Oracle GoldenGate, AWS DMS for CDC; reconciliation jobs.</li>
  <li><strong>Legacy integration</strong> — ACL services, MQ bridges, z/OS Connect for mainframes.</li>
</ul>

<p>Strangler-style migrations are the norm for banks moving off monolithic platforms: digital channels, statements, notifications, and document services move first, while the core ledger is strangled last, behind ACLs and CDC — the same approach I’ve seen work when re-architecting a legacy content-management platform into microservices that served both old SOAP consumers and new REST APIs during the transition.</p>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/anti-corruption-layer/">Anti-Corruption Layer</a> — protects new services from the legacy model.</li>
  <li><a href="/blog/api-gateway/">API Gateway</a> — the usual routing facade.</li>
  <li><a href="/blog/change-data-capture/">Change Data Capture</a> — data sync during migration.</li>
  <li><a href="/blog/shared-database/">Shared Database</a> — the starting point you’re often migrating from.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>Put a routing facade in front of legacy, then move one capability at a time.</li>
  <li>Stop building new features in legacy; start with valuable, well-bounded slices.</li>
  <li>Keep one source of truth per data set per phase; sync one way with CDC; reconcile.</li>
  <li>De-risk with shadow traffic, parallel runs, and gradual rollout — and finish by deleting the old system.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Integration" /><category term="strangler-fig" /><category term="migration" /><category term="monolith" /><category term="modernization" /><category term="legacy" /><category term="incremental" /><summary type="html"><![CDATA[Strangler Fig pattern explained: gradually replaces a monolith. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Anti-Corruption Layer: Keep Legacy Models Out of Your Domain</title><link href="https://karthiksukumaran.com/blog/anti-corruption-layer/" rel="alternate" type="text/html" title="Anti-Corruption Layer: Keep Legacy Models Out of Your Domain" /><published>2026-10-17T09:45:00+05:30</published><updated>2026-10-17T09:45:00+05:30</updated><id>https://karthiksukumaran.com/blog/anti-corruption-layer</id><content type="html" xml:base="https://karthiksukumaran.com/blog/anti-corruption-layer/"><![CDATA[<p>Every enterprise has a system whose model you didn’t choose: a mainframe core-banking platform with 8-character field names and status codes like <code class="language-plaintext highlighter-rouge">"A"</code>, <code class="language-plaintext highlighter-rouge">"C"</code>, <code class="language-plaintext highlighter-rouge">"9"</code>; a 20-year-old ERP; a SaaS CRM with its own idea of a “customer”; a partner API that returns everything as strings. If your new services consume those models directly, the legacy concepts <strong>leak</strong> into your domain — and soon your shiny microservice speaks mainframe.</p>

<p>The <strong>Anti-Corruption Layer (ACL)</strong>, from Eric Evans’s <em>Domain-Driven Design</em>, is a <strong>translation boundary</strong>: it talks to the other system in <em>its</em> language and presents a clean interface in <em>yours</em>.</p>

<h2 id="the-problem">The problem</h2>

<p>Integrating directly with a foreign model causes:</p>

<ul>
  <li><strong>Semantic leakage</strong> — <code class="language-plaintext highlighter-rouge">CUST_STAT_CD = '9'</code> means “deceased” in one system and “dormant” in another; your code fills up with magic values.</li>
  <li><strong>Structural coupling</strong> — your entities mirror their tables and fields.</li>
  <li><strong>Change amplification</strong> — when the legacy system changes, every consumer changes.</li>
  <li><strong>Migration paralysis</strong> — you can’t replace the legacy system because everything depends on its model.</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<pre><code class="language-mermaid">flowchart TD
    subgraph New["Your bounded context — clean domain model"]
      D[Account Opening service&lt;br/&gt;Customer, Account, KycStatus]
    end
    subgraph ACL["Anti-Corruption Layer"]
      F[Facade&lt;br/&gt;domain-shaped interface]
      T[Translator&lt;br/&gt;model ↔ model mapping]
      A[Adapter&lt;br/&gt;protocol: SOAP / MQ / fixed-width files]
      F --&gt; T --&gt; A
    end
    subgraph Legacy["Legacy core banking"]
      L[(CIF records, status codes,&lt;br/&gt;EBCDIC copybooks)]
    end
    D --&gt; F
    A --&gt; L
</code></pre>

<p>An ACL typically has three parts:</p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Responsibility</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Facade</strong></td>
      <td>An interface expressed in <em>your</em> domain language (<code class="language-plaintext highlighter-rouge">CustomerDirectory.findByNationalId(...)</code>)</td>
    </tr>
    <tr>
      <td><strong>Translator</strong></td>
      <td>Maps concepts and values both ways: codes → enums, flat records → aggregates, error codes → domain exceptions</td>
    </tr>
    <tr>
      <td><strong>Adapter</strong></td>
      <td>Handles the foreign protocol and format: SOAP, MQ messages, fixed-width files, proprietary SDKs, pagination quirks</td>
    </tr>
  </tbody>
</table>

<p>The ACL can be a module inside your service or a <strong>separate translation service</strong> shared by several consumers.</p>

<h2 id="example-translating-a-core-banking-customer">Example: translating a core-banking customer</h2>

<p>The legacy system returns:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"CIF_NO"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0004418823"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"CUST_NM"</span><span class="p">:</span><span class="w"> </span><span class="s2">"RAO/ANANYA"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"CUST_TYP"</span><span class="p">:</span><span class="w"> </span><span class="s2">"I"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"STAT_CD"</span><span class="p">:</span><span class="w"> </span><span class="s2">"A"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"KYC_FLG"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Y"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"KYC_DT"</span><span class="p">:</span><span class="w"> </span><span class="s2">"20240311"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"RISK_CAT"</span><span class="p">:</span><span class="w"> </span><span class="s2">"02"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"BR_CD"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0412"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Your domain wants:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">Customer</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">id</span><span class="p">:</span> <span class="nc">CustomerId</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">PersonName</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">type</span><span class="p">:</span> <span class="nc">CustomerType</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">status</span><span class="p">:</span> <span class="nc">CustomerStatus</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">kyc</span><span class="p">:</span> <span class="nc">KycStatus</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">riskRating</span><span class="p">:</span> <span class="nc">RiskRating</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">homeBranch</span><span class="p">:</span> <span class="nc">BranchId</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The ACL:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Facade — the only thing the domain sees</span>
<span class="kd">interface</span> <span class="nc">CustomerDirectory</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">find</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">CustomerId</span><span class="p">):</span> <span class="nc">Customer</span><span class="p">?</span>
<span class="p">}</span>

<span class="c1">// Implementation lives in the ACL module</span>
<span class="kd">class</span> <span class="nc">CoreBankingCustomerDirectory</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">adapter</span><span class="p">:</span> <span class="nc">CoreBankingSoapAdapter</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">translator</span><span class="p">:</span> <span class="nc">CustomerTranslator</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">CustomerDirectory</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">find</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">CustomerId</span><span class="p">):</span> <span class="nc">Customer</span><span class="p">?</span> <span class="p">=</span>
        <span class="n">adapter</span><span class="p">.</span><span class="nf">inquireCif</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="nf">padStart</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="sc">'0'</span><span class="p">))</span>      <span class="c1">// legacy quirks stay here</span>
            <span class="o">?.</span><span class="nf">let</span><span class="p">(</span><span class="n">translator</span><span class="o">::</span><span class="n">toDomain</span><span class="p">)</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">CustomerTranslator</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">clock</span><span class="p">:</span> <span class="nc">Clock</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">toDomain</span><span class="p">(</span><span class="n">r</span><span class="p">:</span> <span class="nc">CifRecord</span><span class="p">):</span> <span class="nc">Customer</span> <span class="p">=</span> <span class="nc">Customer</span><span class="p">(</span>
        <span class="n">id</span> <span class="p">=</span> <span class="nc">CustomerId</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">cifNo</span><span class="p">.</span><span class="nf">trimStart</span><span class="p">(</span><span class="sc">'0'</span><span class="p">)),</span>
        <span class="n">name</span> <span class="p">=</span> <span class="n">r</span><span class="p">.</span><span class="n">custNm</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">).</span><span class="nf">let</span> <span class="p">{</span> <span class="p">(</span><span class="n">last</span><span class="p">,</span> <span class="n">first</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">PersonName</span><span class="p">(</span><span class="n">first</span><span class="p">.</span><span class="nf">titleCase</span><span class="p">(),</span> <span class="n">last</span><span class="p">.</span><span class="nf">titleCase</span><span class="p">())</span> <span class="p">},</span>
        <span class="n">type</span> <span class="p">=</span> <span class="k">when</span> <span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">custTyp</span><span class="p">)</span> <span class="p">{</span>
            <span class="s">"I"</span> <span class="p">-&gt;</span> <span class="nc">CustomerType</span><span class="p">.</span><span class="nc">INDIVIDUAL</span>
            <span class="s">"C"</span> <span class="p">-&gt;</span> <span class="nc">CustomerType</span><span class="p">.</span><span class="nc">CORPORATE</span>
            <span class="k">else</span> <span class="p">-&gt;</span> <span class="k">throw</span> <span class="nc">UnknownLegacyValue</span><span class="p">(</span><span class="s">"CUST_TYP"</span><span class="p">,</span> <span class="n">r</span><span class="p">.</span><span class="n">custTyp</span><span class="p">)</span>
        <span class="p">},</span>
        <span class="n">status</span> <span class="p">=</span> <span class="k">when</span> <span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">statCd</span><span class="p">)</span> <span class="p">{</span>
            <span class="s">"A"</span> <span class="p">-&gt;</span> <span class="nc">CustomerStatus</span><span class="p">.</span><span class="nc">ACTIVE</span>
            <span class="s">"D"</span> <span class="p">-&gt;</span> <span class="nc">CustomerStatus</span><span class="p">.</span><span class="nc">DORMANT</span>
            <span class="s">"C"</span> <span class="p">-&gt;</span> <span class="nc">CustomerStatus</span><span class="p">.</span><span class="nc">CLOSED</span>
            <span class="s">"9"</span> <span class="p">-&gt;</span> <span class="nc">CustomerStatus</span><span class="p">.</span><span class="nc">DECEASED</span>
            <span class="k">else</span> <span class="p">-&gt;</span> <span class="k">throw</span> <span class="nc">UnknownLegacyValue</span><span class="p">(</span><span class="s">"STAT_CD"</span><span class="p">,</span> <span class="n">r</span><span class="p">.</span><span class="n">statCd</span><span class="p">)</span>
        <span class="p">},</span>
        <span class="n">kyc</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">kycFlg</span> <span class="p">==</span> <span class="s">"Y"</span><span class="p">)</span>
            <span class="nc">KycStatus</span><span class="p">.</span><span class="nc">Verified</span><span class="p">(</span><span class="nc">LocalDate</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">kycDt</span><span class="p">,</span> <span class="nc">DateTimeFormatter</span><span class="p">.</span><span class="nc">BASIC_ISO_DATE</span><span class="p">))</span>
        <span class="k">else</span> <span class="nc">KycStatus</span><span class="p">.</span><span class="nc">Pending</span><span class="p">,</span>
        <span class="n">riskRating</span> <span class="p">=</span> <span class="nc">RiskRating</span><span class="p">.</span><span class="nf">fromScore</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">riskCat</span><span class="p">.</span><span class="nf">toInt</span><span class="p">()),</span>
        <span class="n">homeBranch</span> <span class="p">=</span> <span class="nc">BranchId</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">brCd</span><span class="p">),</span>
    <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>All the legacy weirdness — zero-padding, slash-separated names, single-letter codes, <code class="language-plaintext highlighter-rouge">yyyyMMdd</code> dates — is confined to one place. The domain never sees <code class="language-plaintext highlighter-rouge">STAT_CD</code>.</p>

<h2 id="two-directions">Two directions</h2>

<p>ACLs translate <strong>both</strong> ways:</p>

<ul>
  <li><strong>Inbound</strong> — legacy data and events → your domain objects/events (e.g., translating CDC records from legacy tables into clean <a href="/blog/domain-events/">Domain Events</a>).</li>
  <li><strong>Outbound</strong> — your commands → legacy transactions (e.g., <code class="language-plaintext highlighter-rouge">OpenAccount</code> → a sequence of mainframe calls with the right codes).</li>
</ul>

<pre><code class="language-mermaid">flowchart TD
    E[Legacy table change&lt;br/&gt;via CDC] --&gt; ACL1[ACL translator]
    ACL1 --&gt; DE([CustomerKycVerified domain event])
    CMD([OpenAccount command]) --&gt; ACL2[ACL translator]
    ACL2 --&gt; MQ[Legacy MQ transaction&lt;br/&gt;ACCT-OPN-01]
</code></pre>

<h2 id="testing-the-acl">Testing the ACL</h2>

<p>The ACL is where integration bugs hide, so test it heavily:</p>

<ul>
  <li><strong>Translation unit tests</strong> — table-driven tests for every code and edge case (blank fields, unknown codes, invalid dates).</li>
  <li><strong>Contract tests</strong> — record real legacy responses (sanitized) and replay them; alert when the legacy system starts returning unexpected values.</li>
  <li><strong>Fail loudly on unknown values</strong> — an unrecognized status code should raise an error and an alert, not silently map to a default.</li>
</ul>

<h2 id="when-is-an-acl-worth-it">When is an ACL worth it?</h2>

<table>
  <thead>
    <tr>
      <th>Situation</th>
      <th>ACL?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Legacy/external model differs substantially from yours</td>
      <td><strong>Yes</strong></td>
    </tr>
    <tr>
      <td>You plan to replace the legacy system (strangler migration)</td>
      <td><strong>Yes</strong> — swap the adapter, keep the facade</td>
    </tr>
    <tr>
      <td>Multiple new services consume the same legacy system</td>
      <td><strong>Yes</strong> — consider a shared translation service</td>
    </tr>
    <tr>
      <td>The external model is a well-designed, stable public standard (e.g., ISO 20022)</td>
      <td>Maybe — you might adopt it as a shared kernel</td>
    </tr>
    <tr>
      <td>Thin CRUD integration with a simple, aligned model</td>
      <td>Probably not — a small adapter suffices</td>
    </tr>
  </tbody>
</table>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Domain model stays clean and expressive</td>
      <td>Extra layer to build and maintain</td>
    </tr>
    <tr>
      <td>Legacy changes are absorbed in one place</td>
      <td>Translation adds latency and possible data loss</td>
    </tr>
    <tr>
      <td>Enables gradual replacement of legacy systems</td>
      <td>Can become a bottleneck if one team owns a shared ACL</td>
    </tr>
    <tr>
      <td>Centralizes knowledge of legacy quirks</td>
      <td>Requires real understanding of both models</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> a “pass-through” ACL that just renames fields (<code class="language-plaintext highlighter-rouge">CUST_NM</code> → <code class="language-plaintext highlighter-rouge">custName</code>) and passes legacy codes straight through. The legacy <em>concepts</em> still leak. Translate meaning, not just spelling.</p>
</blockquote>

<ul>
  <li><strong>Leaky abstractions</strong> — facade methods that mirror legacy transactions (<code class="language-plaintext highlighter-rouge">inquireCif</code>) rather than domain needs.</li>
  <li><strong>Silent defaults</strong> for unknown codes, hiding data-quality problems.</li>
  <li><strong>Business logic in the ACL</strong> — it should translate, not decide.</li>
  <li><strong>Bypassing the ACL</strong> “just this once” for a quick feature.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>Adapter services</strong> written in the team’s main language, often with mapping libraries (MapStruct, AutoMapper).</li>
  <li><strong>Integration frameworks</strong> — Apache Camel, Spring Integration, MuleSoft and other iPaaS tools for protocol and format conversion.</li>
  <li><strong>Mainframe integration</strong> — IBM MQ, CICS web services, z/OS Connect APIs wrapped behind ACLs.</li>
</ul>

<p>In banking modernization programmes, ACLs around core banking, card management, and document management platforms are what let new digital channels move fast without inheriting decades of legacy semantics.</p>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/strangler-fig/">Strangler Fig</a> — the ACL keeps the new system clean while the old one is replaced.</li>
  <li><a href="/blog/change-data-capture/">Change Data Capture</a> — feed legacy changes through an ACL into domain events.</li>
  <li><a href="/blog/shared-database/">Shared Database</a> — what ACLs help you avoid depending on.</li>
  <li><a href="/blog/backend-for-frontend/">Backend for Frontend</a> — a client-facing adapter, often in front of ACL-protected services.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>An ACL translates between a foreign model and your domain, in both directions.</li>
  <li>Structure it as facade (your language), translator (meaning), adapter (protocol).</li>
  <li>Translate concepts, not just field names, and fail loudly on unknown values.</li>
  <li>It’s the enabler for replacing legacy systems incrementally.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Integration" /><category term="anti-corruption-layer" /><category term="ddd" /><category term="legacy" /><category term="adapter" /><category term="translation" /><category term="integration" /><summary type="html"><![CDATA[Anti-Corruption Layer pattern explained: translates a legacy or external model. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Correlation ID: One Identifier to Follow a Request Everywhere</title><link href="https://karthiksukumaran.com/blog/correlation-id/" rel="alternate" type="text/html" title="Correlation ID: One Identifier to Follow a Request Everywhere" /><published>2026-10-17T09:44:00+05:30</published><updated>2026-10-17T09:44:00+05:30</updated><id>https://karthiksukumaran.com/blog/correlation-id</id><content type="html" xml:base="https://karthiksukumaran.com/blog/correlation-id/"><![CDATA[<p>A customer calls support: <em>“My payment failed at 10:42.”</em> The request touched the gateway, the checkout service, payments, fraud, a Kafka topic, a ledger consumer, and an email worker — each with its own logs on dozens of pods. Without a common identifier, you’re grepping timestamps and guessing.</p>

<p>A <strong>Correlation ID</strong> is a unique identifier assigned when a request (or business transaction) enters the system and <strong>propagated to every service, message, and log line</strong> it causes. Search for it once and you see the whole story.</p>

<h2 id="the-problem">The problem</h2>

<p>In a monolith, one request is one thread with one log context. In a distributed system:</p>

<ul>
  <li>one user action fans out into many synchronous calls,</li>
  <li>asynchronous messages trigger work minutes later in other services,</li>
  <li>retries and redeliveries repeat work,</li>
  <li>and logs are spread across services, pods, and time.</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<pre><code class="language-mermaid">sequenceDiagram
    participant C as Client
    participant G as API Gateway
    participant A as Checkout
    participant P as Payments
    participant K as Kafka
    participant L as Ledger consumer
    C-&gt;&gt;G: POST /checkout
    Note over G: no incoming ID → generate&lt;br/&gt;traceparent 00-4bf9…-01
    G-&gt;&gt;A: traceparent: 00-4bf9…
    A-&gt;&gt;P: traceparent: 00-4bf9…
    P-&gt;&gt;K: message headers: traceparent 00-4bf9…
    K-&gt;&gt;L: (later) same header
    Note over A,L: every log line includes trace_id=4bf9…
    G--&gt;&gt;C: 200 + header X-Request-ID / traceresponse
</code></pre>

<p>Four rules:</p>

<ol>
  <li><strong>Generate at the edge</strong> if the incoming request has none (and decide whether to trust IDs from external clients).</li>
  <li><strong>Propagate on every outbound call</strong> — HTTP headers, gRPC metadata, message headers.</li>
  <li><strong>Put it in every log line</strong> via the logging context (MDC, scopes, structured fields).</li>
  <li><strong>Return it to the caller</strong> so users and support can quote it.</li>
</ol>

<h2 id="which-header">Which header?</h2>

<table>
  <thead>
    <tr>
      <th>Header</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">traceparent</code></strong> (W3C Trace Context)</td>
      <td>The standard. Carries trace ID, parent span ID, and sampling flags. Use this.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tracestate</code></td>
      <td>Vendor-specific additions alongside <code class="language-plaintext highlighter-rouge">traceparent</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">X-Request-ID</code> / <code class="language-plaintext highlighter-rouge">X-Correlation-ID</code></td>
      <td>Common legacy conventions; still useful as a human-friendly ID or for systems without tracing.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">b3</code> / <code class="language-plaintext highlighter-rouge">X-B3-*</code></td>
      <td>Zipkin’s older format; supported by many tools for compatibility.</td>
    </tr>
  </tbody>
</table>

<p>With <a href="/blog/distributed-tracing/">Distributed Tracing</a> via OpenTelemetry, the <strong>trace ID is your correlation ID</strong> — you get propagation for free, plus timing. Many teams still keep a separate <strong>business correlation ID</strong> (e.g., <code class="language-plaintext highlighter-rouge">orderId</code> or <code class="language-plaintext highlighter-rouge">checkoutSessionId</code>) that spans <em>multiple</em> traces over hours or days.</p>

<table>
  <thead>
    <tr>
      <th>ID type</th>
      <th>Scope</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Trace ID</strong></td>
      <td>One request tree (ms–seconds)</td>
      <td><code class="language-plaintext highlighter-rouge">4bf92f3577b34da6a3ce929d0e0e4736</code></td>
    </tr>
    <tr>
      <td><strong>Business correlation ID</strong></td>
      <td>One business process across many requests/messages (minutes–weeks)</td>
      <td><code class="language-plaintext highlighter-rouge">checkout-7f3a</code>, <code class="language-plaintext highlighter-rouge">loan-application-7781</code></td>
    </tr>
    <tr>
      <td><strong>Causation ID</strong></td>
      <td>“Which message caused this one?”</td>
      <td>Message ID of the triggering event</td>
    </tr>
  </tbody>
</table>

<p>Event-driven systems often carry all three in message metadata: <code class="language-plaintext highlighter-rouge">correlationId</code>, <code class="language-plaintext highlighter-rouge">causationId</code>, and <code class="language-plaintext highlighter-rouge">messageId</code>.</p>

<h2 id="example-spring-boot-filter--mdc">Example: Spring Boot filter + MDC</h2>

<p>With OpenTelemetry’s Java agent or Micrometer Tracing, <code class="language-plaintext highlighter-rouge">traceId</code> and <code class="language-plaintext highlighter-rouge">spanId</code> are put in the MDC automatically. Here’s the manual version, which also shows the moving parts:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="nd">@Order</span><span class="o">(</span><span class="nc">Ordered</span><span class="o">.</span><span class="na">HIGHEST_PRECEDENCE</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CorrelationIdFilter</span> <span class="kd">extends</span> <span class="nc">OncePerRequestFilter</span> <span class="o">{</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">HEADER</span> <span class="o">=</span> <span class="s">"X-Correlation-ID"</span><span class="o">;</span>

  <span class="nd">@Override</span>
  <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">req</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">res</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">)</span>
      <span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">incoming</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="no">HEADER</span><span class="o">);</span>
    <span class="nc">String</span> <span class="n">id</span> <span class="o">=</span> <span class="n">isValid</span><span class="o">(</span><span class="n">incoming</span><span class="o">)</span> <span class="o">?</span> <span class="n">incoming</span> <span class="o">:</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>
    <span class="no">MDC</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"correlationId"</span><span class="o">,</span> <span class="n">id</span><span class="o">);</span>
    <span class="n">res</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="no">HEADER</span><span class="o">,</span> <span class="n">id</span><span class="o">);</span>                 <span class="c1">// return it to the caller</span>
    <span class="k">try</span> <span class="o">{</span>
      <span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">req</span><span class="o">,</span> <span class="n">res</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
      <span class="no">MDC</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="s">"correlationId"</span><span class="o">);</span>             <span class="c1">// threads are pooled: always clean up</span>
    <span class="o">}</span>
  <span class="o">}</span>

  <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isValid</span><span class="o">(</span><span class="nc">String</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span>          <span class="c1">// don't trust arbitrary input into logs</span>
    <span class="k">return</span> <span class="n">v</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">v</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">&lt;=</span> <span class="mi">64</span> <span class="o">&amp;&amp;</span> <span class="n">v</span><span class="o">.</span><span class="na">matches</span><span class="o">(</span><span class="s">"[A-Za-z0-9-]+"</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// Propagate on outbound HTTP calls</span>
<span class="nd">@Bean</span>
<span class="nc">RestClient</span> <span class="nf">restClient</span><span class="o">(</span><span class="nc">RestClient</span><span class="o">.</span><span class="na">Builder</span> <span class="n">b</span><span class="o">)</span> <span class="o">{</span>
  <span class="k">return</span> <span class="n">b</span><span class="o">.</span><span class="na">requestInterceptor</span><span class="o">((</span><span class="n">request</span><span class="o">,</span> <span class="n">body</span><span class="o">,</span> <span class="n">exec</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">id</span> <span class="o">=</span> <span class="no">MDC</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"correlationId"</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">id</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeaders</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="nc">CorrelationIdFilter</span><span class="o">.</span><span class="na">HEADER</span><span class="o">,</span> <span class="n">id</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">exec</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">body</span><span class="o">);</span>
  <span class="o">}).</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- logback-spring.xml: JSON logs with correlation fields --&gt;</span>
<span class="nt">&lt;encoder</span> <span class="na">class=</span><span class="s">"net.logstash.logback.encoder.LogstashEncoder"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;includeMdcKeyName&gt;</span>correlationId<span class="nt">&lt;/includeMdcKeyName&gt;</span>
  <span class="nt">&lt;includeMdcKeyName&gt;</span>traceId<span class="nt">&lt;/includeMdcKeyName&gt;</span>
  <span class="nt">&lt;includeMdcKeyName&gt;</span>spanId<span class="nt">&lt;/includeMdcKeyName&gt;</span>
<span class="nt">&lt;/encoder&gt;</span>
</code></pre></div></div>

<h2 id="across-asynchronous-hops">Across asynchronous hops</h2>

<p>Propagation breaks most often at message boundaries and thread pools.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Producer: copy context into message headers (OpenTelemetry instrumentation does this automatically)</span>
<span class="nc">ProducerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderPlaced</span><span class="o">&gt;</span> <span class="n">rec</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ProducerRecord</span><span class="o">&lt;&gt;(</span><span class="s">"orders.events"</span><span class="o">,</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">(),</span> <span class="n">event</span><span class="o">);</span>
<span class="n">rec</span><span class="o">.</span><span class="na">headers</span><span class="o">().</span><span class="na">add</span><span class="o">(</span><span class="s">"correlationId"</span><span class="o">,</span> <span class="no">MDC</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"correlationId"</span><span class="o">).</span><span class="na">getBytes</span><span class="o">(</span><span class="no">UTF_8</span><span class="o">));</span>
<span class="n">rec</span><span class="o">.</span><span class="na">headers</span><span class="o">().</span><span class="na">add</span><span class="o">(</span><span class="s">"causationId"</span><span class="o">,</span> <span class="n">currentMessageIdOrRequestId</span><span class="o">().</span><span class="na">getBytes</span><span class="o">(</span><span class="no">UTF_8</span><span class="o">));</span>
<span class="n">kafka</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="n">rec</span><span class="o">);</span>

<span class="c1">// Consumer: restore context before processing</span>
<span class="nd">@KafkaListener</span><span class="o">(</span><span class="n">topics</span> <span class="o">=</span> <span class="s">"orders.events"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">on</span><span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderPlaced</span><span class="o">&gt;</span> <span class="n">rec</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">Header</span> <span class="n">h</span> <span class="o">=</span> <span class="n">rec</span><span class="o">.</span><span class="na">headers</span><span class="o">().</span><span class="na">lastHeader</span><span class="o">(</span><span class="s">"correlationId"</span><span class="o">);</span>
  <span class="k">try</span> <span class="o">(</span><span class="no">MDC</span><span class="o">.</span><span class="na">MDCCloseable</span> <span class="n">c</span> <span class="o">=</span> <span class="no">MDC</span><span class="o">.</span><span class="na">putCloseable</span><span class="o">(</span><span class="s">"correlationId"</span><span class="o">,</span>
          <span class="n">h</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="n">h</span><span class="o">.</span><span class="na">value</span><span class="o">(),</span> <span class="no">UTF_8</span><span class="o">)</span> <span class="o">:</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">()))</span> <span class="o">{</span>
    <span class="n">handle</span><span class="o">(</span><span class="n">rec</span><span class="o">.</span><span class="na">value</span><span class="o">());</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Also:</p>

<ul>
  <li><strong>Thread pools / <code class="language-plaintext highlighter-rouge">CompletableFuture</code></strong> — wrap executors so context is copied (OpenTelemetry’s <code class="language-plaintext highlighter-rouge">Context.taskWrapping</code>, Micrometer’s context propagation).</li>
  <li><strong>Outbox rows</strong> — store correlation and trace headers in the outbox so the relay can attach them (<a href="/blog/transactional-outbox/">Transactional Outbox</a>).</li>
  <li><strong>Batch jobs</strong> — assign a correlation ID per run and per item.</li>
</ul>

<h2 id="security-and-privacy">Security and privacy</h2>

<ul>
  <li><strong>Validate incoming IDs</strong> (length, character set) to prevent log injection.</li>
  <li><strong>Don’t encode personal data</strong> in correlation IDs (no emails, phone numbers, account numbers).</li>
  <li><strong>Decide trust boundaries</strong> — you may accept <code class="language-plaintext highlighter-rouge">traceparent</code> from internal callers but start a new trace (linking the external one) at the public edge.</li>
</ul>

<h2 id="trade-offs">Trade-offs</h2>

<table>
  <thead>
    <tr>
      <th>Benefits</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Find every log line for one request in one search</td>
      <td>Requires discipline in every service and library</td>
    </tr>
    <tr>
      <td>Essential for support, incident response, and audit</td>
      <td>Propagation gaps at async boundaries</td>
    </tr>
    <tr>
      <td>Low cost — a header and a log field</td>
      <td>Untrusted input if accepted blindly from clients</td>
    </tr>
    <tr>
      <td>Foundation for distributed tracing</td>
      <td>Multiple ID types to explain to teams</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls">Common pitfalls</h2>

<blockquote>
  <p><strong>Pitfall:</strong> generating a new ID in every service. Each service logs its own UUID, and nothing connects them. Generate once at the edge; everyone else <strong>propagates</strong>.</p>
</blockquote>

<ul>
  <li><strong>Losing the ID at the message broker</strong> — producers don’t set headers, consumers don’t restore them.</li>
  <li><strong>MDC leaks</strong> — not clearing context on pooled threads, so the next request logs the previous ID.</li>
  <li><strong>Unstructured logs</strong> — the ID is in the text but not a field, making search slow and unreliable.</li>
  <li><strong>Not returning the ID</strong> to clients, so support can’t ask for it.</li>
</ul>

<h2 id="tools-and-real-world-usage">Tools and real-world usage</h2>

<ul>
  <li><strong>OpenTelemetry</strong> SDKs and auto-instrumentation — W3C Trace Context propagation across HTTP, gRPC, and messaging.</li>
  <li><strong>Micrometer Tracing</strong> (Spring Boot 3), <strong>Serilog</strong> enrichers and <code class="language-plaintext highlighter-rouge">Activity</code> in .NET, <strong>pino</strong>/<strong>winston</strong> with AsyncLocalStorage in Node.js.</li>
  <li><strong>API gateways</strong> (Kong <code class="language-plaintext highlighter-rouge">correlation-id</code> plugin, NGINX <code class="language-plaintext highlighter-rouge">$request_id</code>, Envoy <code class="language-plaintext highlighter-rouge">x-request-id</code>) — generate at the edge.</li>
  <li>Log platforms (ELK, Loki, Splunk, Datadog) — pivot from a correlation ID to all related logs and traces.</li>
</ul>

<h2 id="related-patterns">Related patterns</h2>

<ul>
  <li><a href="/blog/distributed-tracing/">Distributed Tracing</a> — correlation plus timing and causality.</li>
  <li><a href="/blog/log-aggregation/">Log Aggregation</a> — where correlation IDs become searchable.</li>
  <li><a href="/blog/async-request-reply/">Async Request-Reply</a> — correlation IDs match replies to requests.</li>
  <li><a href="/blog/saga-pattern/">Saga</a> — business correlation IDs follow long-running processes.</li>
</ul>

<h2 id="key-takeaways">Key takeaways</h2>

<ol>
  <li>Generate once at the edge, propagate everywhere, log it in every line, return it to the caller.</li>
  <li>Prefer W3C <code class="language-plaintext highlighter-rouge">traceparent</code>; add a business correlation ID for long processes.</li>
  <li>Carry IDs through message headers, outbox rows, and thread pools.</li>
  <li>Validate incoming IDs and keep personal data out of them.</li>
</ol>]]></content><author><name>Karthik Sukumaran</name></author><category term="Integration" /><category term="correlation-id" /><category term="traceparent" /><category term="logging" /><category term="mdc" /><category term="observability" /><category term="propagation" /><summary type="html"><![CDATA[Correlation ID pattern explained: one ID tracks a flow across services. How it works, diagrams, code examples, trade-offs and common pitfalls.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://karthiksukumaran.com/images/social-card.jpg" /><media:content medium="image" url="https://karthiksukumaran.com/images/social-card.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>