<?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://abhishek-shah.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://abhishek-shah.dev/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-06-27T09:59:58+00:00</updated><id>https://abhishek-shah.dev/feed.xml</id><title type="html">Abhishek Shah</title><subtitle>Abhishek Shah is a backend engineer with 11+ years of experience building APIs and data systems for the finance industry. Writing about backend engineering, distributed systems, and the mathematics behind AI.</subtitle><author><name>Abhishek Shah</name></author><entry><title type="html">Our First Windows 98 PC</title><link href="https://abhishek-shah.dev/blog/2026/06/27/our-first-windows-98-pc/" rel="alternate" type="text/html" title="Our First Windows 98 PC" /><published>2026-06-27T00:00:00+00:00</published><updated>2026-06-27T00:00:00+00:00</updated><id>https://abhishek-shah.dev/blog/2026/06/27/our-first-windows-98-pc</id><content type="html" xml:base="https://abhishek-shah.dev/blog/2026/06/27/our-first-windows-98-pc/"><![CDATA[<p><img src="/assets/images/win98-desktop-nostalgia.jpeg" alt="A Windows 98 desktop with a mountain lion wallpaper, surrounded by icons for Quake, Diablo, Duke Nukem 3D, Encarta 99, and AOL Instant Messenger" /></p>
<p class="post-meta">Image credit: <a href="https://twitter.com/RetroTechDreams" target="_blank" rel="noopener noreferrer">Retro Tech Dreams</a></p>

<p>I was scrolling through old tech nostalgia pictures online and ran into this screenshot of a classic Windows 98 desktop, mountain lion wallpaper, AIM running man, Encarta 99, the works. It stopped me mid-scroll, because it looked almost exactly like the desktop I grew up with.</p>

<p>In January 2000, my father brought home our first computer: an assembled Pentium III, somewhere between 2 and 5 GB of hard drive space, and 128MB of RAM. It had a Creative sound card installed, which felt like a big deal at the time, everything suddenly had real sound instead of beeps. I was 9 years old, and that machine became the center of my world for the next few years.</p>

<p>I spent hours on Need for Speed, Sonic, a Star Wars game I only remember as “Sith,” and whatever “ultra fighter” game I could get running. In between the games, I quietly taught myself PowerPoint and Word, no one showed me, I just clicked around until things made sense. It’s funny looking back now, since that’s still basically how I learn most software today.</p>

<p>That PC broke down more than once over the years. Each time, my father just took it in and got it repaired rather than giving up on it. I didn’t think much of it then, but it’s clear now that those repairs were him making sure I kept having a machine to learn on.</p>

<p>Finding that screenshot today was a small thing, but it pulled up a lot. That beige tower with the Creative sound card was the start of a path that eventually turned into a career in software.</p>]]></content><author><name>Abhishek Shah</name></author><summary type="html"><![CDATA[Stumbling on a Windows 98 desktop screenshot and remembering the Pentium III my father brought home in January 2000.]]></summary></entry><entry><title type="html">Gradient Descent Variants: Momentum, RMSProp, and Adam Explained</title><link href="https://abhishek-shah.dev/blog/2026/06/20/gradient-descent-variants/" rel="alternate" type="text/html" title="Gradient Descent Variants: Momentum, RMSProp, and Adam Explained" /><published>2026-06-20T00:00:00+00:00</published><updated>2026-06-20T00:00:00+00:00</updated><id>https://abhishek-shah.dev/blog/2026/06/20/gradient-descent-variants</id><content type="html" xml:base="https://abhishek-shah.dev/blog/2026/06/20/gradient-descent-variants/"><![CDATA[<p>Vanilla gradient descent updates each weight by a fixed step in the direction opposite its gradient: <code class="language-plaintext highlighter-rouge">w = w - lr * grad</code>. It works, but it’s slow to converge and easy to destabilize — and almost nothing trains with it directly anymore. Momentum, RMSProp, and Adam are the practical fixes, and each one solves a specific failure mode of the one before it.</p>

<h2 id="why-vanilla-gradient-descent-struggles">Why Vanilla Gradient Descent Struggles</h2>

<p>Loss surfaces aren’t bowls — they’re full of narrow ravines, where the gradient in one direction is much steeper than in another. With a single fixed learning rate, you either oscillate back and forth across the steep direction, or move too slowly along the shallow one. Picking a learning rate that’s safe for the steep direction makes progress along the shallow direction painfully slow.</p>

<h2 id="momentum">Momentum</h2>

<p>Momentum keeps a running average of past gradients and updates the weight using that average instead of the raw gradient:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>v = beta * v + (1 - beta) * grad
w = w - lr * v
</code></pre></div></div>

<p>Intuitively, it’s a ball rolling downhill: gradients that consistently point the same way accumulate velocity, while oscillating gradients (the back-and-forth across a ravine) cancel out in the average. This damps oscillation and speeds up movement in directions where the gradient is consistent.</p>

<h2 id="rmsprop">RMSProp</h2>

<p>Momentum doesn’t address that different parameters can need very different step sizes. RMSProp tracks a moving average of the <em>squared</em> gradient per parameter, and divides the update by its square root:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>s = beta * s + (1 - beta) * grad^2
w = w - lr * grad / (sqrt(s) + eps)
</code></pre></div></div>

<p>Parameters with consistently large gradients get their effective learning rate shrunk; parameters with small, infrequent gradients get it boosted. Each weight effectively gets its own adaptive learning rate.</p>

<h2 id="adam">Adam</h2>

<p>Adam combines both ideas: momentum for the direction, RMSProp-style scaling for the step size, plus a bias correction term because the moving averages start at zero and are biased low early in training.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>m = beta1 * m + (1 - beta1) * grad
s = beta2 * s + (1 - beta2) * grad^2

m_hat = m / (1 - beta1^t)
s_hat = s / (1 - beta2^t)

w = w - lr * m_hat / (sqrt(s_hat) + eps)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">t</code> is the current training step, and the bias correction terms matter most early on — without them, <code class="language-plaintext highlighter-rouge">m</code> and <code class="language-plaintext highlighter-rouge">s</code> are artificially small for the first several updates.</p>

<h2 id="why-it-matters">Why It Matters</h2>

<p>This is why <code class="language-plaintext highlighter-rouge">optimizer=Adam</code> is the default in nearly every training script: it inherits momentum’s resistance to oscillation and RMSProp’s per-parameter adaptivity, with defaults (<code class="language-plaintext highlighter-rouge">beta1=0.9, beta2=0.999</code>) that work reasonably well across a huge range of problems without manual tuning. Understanding what each piece corrects for makes it much easier to diagnose training instability — a loss that oscillates wildly often means the effective step size is too large for the steepest direction in the loss surface, regardless of which optimizer is doing the stepping.</p>]]></content><author><name>Abhishek Shah</name></author><summary type="html"><![CDATA[A practical, math-first explanation of why vanilla gradient descent struggles in practice, and how momentum, RMSProp, and Adam fix it.]]></summary></entry><entry><title type="html">Event Sourcing for Financial Data: Why Immutable Logs Beat CRUD</title><link href="https://abhishek-shah.dev/blog/2026/06/18/event-sourcing-financial-data/" rel="alternate" type="text/html" title="Event Sourcing for Financial Data: Why Immutable Logs Beat CRUD" /><published>2026-06-18T00:00:00+00:00</published><updated>2026-06-18T00:00:00+00:00</updated><id>https://abhishek-shah.dev/blog/2026/06/18/event-sourcing-financial-data</id><content type="html" xml:base="https://abhishek-shah.dev/blog/2026/06/18/event-sourcing-financial-data/"><![CDATA[<p>Most applications store state the same way: a row represents the current truth, and an <code class="language-plaintext highlighter-rouge">UPDATE</code> overwrites it. That works fine until someone asks “what was this account’s balance last Tuesday?” or “who changed this value, and why?” — questions CRUD can’t answer, because the previous state is gone.</p>

<p>Event sourcing solves this by changing what you store: not the current state, but every event that led to it.</p>

<h2 id="the-core-idea">The Core Idea</h2>

<p>Instead of a <code class="language-plaintext highlighter-rouge">balance</code> column you update in place, you store an append-only log of events:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AccountOpened    { account: "acct_1" }
FundsDeposited   { account: "acct_1", amount: 1000 }
FundsWithdrawn   { account: "acct_1", amount: 200 }
FundsDeposited   { account: "acct_1", amount: 50 }
</code></pre></div></div>

<p>The current balance isn’t stored anywhere — it’s <em>derived</em> by replaying the events: <code class="language-plaintext highlighter-rouge">0 + 1000 - 200 + 50 = 850</code>. The events are the source of truth; every view of “current state” is a projection computed from them.</p>

<h2 id="why-this-fits-financial-systems">Why This Fits Financial Systems</h2>

<ul>
  <li><strong>Audit trail for free.</strong> Regulators and auditors don’t want “the balance is 850” — they want to know how it got there. With event sourcing, the full history is the data model, not a separate audit log bolted on afterward.</li>
  <li><strong>Point-in-time reconstruction.</strong> Replaying events up to a given timestamp reproduces the exact state at that moment, which is exactly what reconciliation and historical reporting need.</li>
  <li><strong>Corrections without overwrites.</strong> If a deposit was recorded in error, you don’t edit history — you append a <code class="language-plaintext highlighter-rouge">FundsDepositReversed</code> event. The mistake and its correction both remain visible.</li>
</ul>

<h2 id="snapshotting">Snapshotting</h2>

<p>Replaying years of events on every read doesn’t scale. The common fix is snapshotting: periodically persist the derived state (e.g., the balance after every 1,000 events), and on read, load the nearest snapshot and replay only the events after it.</p>

<h2 id="cqrs">CQRS</h2>

<p>Event sourcing pairs naturally with Command Query Responsibility Segregation (CQRS): writes append events, while reads are served from one or more projections (materialized views) built by consuming the event stream. This lets you build a fast “current balance” table for queries while keeping the event log as the durable, authoritative history.</p>

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

<p>It isn’t free:</p>

<ul>
  <li><strong>Complexity.</strong> You’re building an event store, projections, and replay logic instead of an <code class="language-plaintext highlighter-rouge">UPDATE</code> statement.</li>
  <li><strong>Eventual consistency.</strong> If projections are updated asynchronously, a read immediately after a write can be stale.</li>
  <li><strong>Schema evolution.</strong> Old events were written under old assumptions; your event handlers need to keep understanding them as the system evolves.</li>
</ul>

<h2 id="when-its-worth-it">When It’s Worth It</h2>

<p>Event sourcing earns its complexity when the history itself is a business requirement — ledgers, payment systems, trading platforms, anything with compliance or audit obligations — rather than systems where “what is the value right now” is the only question anyone ever asks.</p>]]></content><author><name>Abhishek Shah</name></author><summary type="html"><![CDATA[An introduction to event sourcing — storing state as a sequence of immutable events — and why it fits financial and audit-heavy systems better than plain CRUD.]]></summary></entry><entry><title type="html">Idempotency Keys: Designing Safe-to-Retry APIs in Financial Systems</title><link href="https://abhishek-shah.dev/blog/2026/06/16/idempotency-keys-financial-apis/" rel="alternate" type="text/html" title="Idempotency Keys: Designing Safe-to-Retry APIs in Financial Systems" /><published>2026-06-16T00:00:00+00:00</published><updated>2026-06-16T00:00:00+00:00</updated><id>https://abhishek-shah.dev/blog/2026/06/16/idempotency-keys-financial-apis</id><content type="html" xml:base="https://abhishek-shah.dev/blog/2026/06/16/idempotency-keys-financial-apis/"><![CDATA[<p>Networks fail. Clients time out and retry. Load balancers occasionally deliver the same request twice. In most APIs that’s a minor annoyance — in a payments or ledger API, it’s how customers get charged twice.</p>

<p>Idempotency keys are the standard fix, and the pattern shows up everywhere from Stripe’s API to internal settlement systems.</p>

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

<p>Imagine a <code class="language-plaintext highlighter-rouge">POST /transfers</code> endpoint. A client sends a request, the server processes the transfer, but the response is lost to a network blip before the client receives it. The client, seeing a timeout, retries. From the server’s point of view, two valid requests arrived — so it processes the transfer twice.</p>

<p>Retrying is the correct client behavior. The server’s job is to make sure retries are safe.</p>

<h2 id="the-pattern">The Pattern</h2>

<p>The client generates a unique key — typically a UUID — and sends it on every attempt of the same logical operation:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /transfers
Idempotency-Key: 7e3f9c1a-7b2e-4e29-9c34-3a1d9f9b9b40

{ "from": "acct_1", "to": "acct_2", "amount": 500 }
</code></pre></div></div>

<p>On the server:</p>

<ol>
  <li>Look up the key in a dedicated store (a table or cache with a TTL).</li>
  <li>If it doesn’t exist, mark it as “in progress,” process the request, store the result, and return it.</li>
  <li>If it exists and is “in progress,” reject or hold the duplicate request — a concurrent retry shouldn’t process the transfer a second time while the first is still running.</li>
  <li>If it exists and is “complete,” return the stored response without reprocessing anything.</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>key_record = store.get(idempotency_key)

if key_record is None:
    store.put(idempotency_key, status="in_progress")
    result = process_transfer(payload)
    store.update(idempotency_key, status="complete", response=result)
    return result

if key_record.status == "in_progress":
    raise ConflictError("request already in flight")

return key_record.response
</code></pre></div></div>

<h2 id="edge-cases-that-matter">Edge Cases That Matter</h2>

<ul>
  <li><strong>Key reuse with a different payload.</strong> A client might accidentally send the same key with a different amount. Store a hash of the request body alongside the key, and reject mismatches — otherwise you silently return the wrong result for the second request.</li>
  <li><strong>Race conditions.</strong> Two retries can arrive within milliseconds of each other. The “in progress” state needs an atomic write (a unique constraint in a relational table, or a conditional <code class="language-plaintext highlighter-rouge">SETNX</code> in a key-value store) so only one request wins the race.</li>
  <li><strong>Expiry.</strong> Keys can’t live forever. A TTL of 24 hours is common — long enough to cover realistic retry windows, short enough to bound storage growth.</li>
  <li><strong>Scope.</strong> Keys should be scoped per client or per API credential, not global, to avoid collisions between unrelated callers.</li>
</ul>

<h2 id="why-it-matters">Why It Matters</h2>

<p>This is exactly how Stripe, Adyen, and most payment processors prevent double charges, and it’s a standard expectation for any API that mutates money, inventory, or anything else that can’t simply be re-run. If you’re building APIs for a finance system and retries aren’t idempotent, you have a double-processing bug waiting to happen in production — it’s just a matter of when a slow network finds it.</p>]]></content><author><name>Abhishek Shah</name></author><summary type="html"><![CDATA[Why financial APIs need idempotency keys, how they work under the hood, and patterns for implementing them safely at scale.]]></summary></entry><entry><title type="html">Understanding Backpropagation from Scratch</title><link href="https://abhishek-shah.dev/blog/2026/06/14/understanding-backpropagation/" rel="alternate" type="text/html" title="Understanding Backpropagation from Scratch" /><published>2026-06-14T00:00:00+00:00</published><updated>2026-06-14T00:00:00+00:00</updated><id>https://abhishek-shah.dev/blog/2026/06/14/understanding-backpropagation</id><content type="html" xml:base="https://abhishek-shah.dev/blog/2026/06/14/understanding-backpropagation/"><![CDATA[<p>Backpropagation is the algorithm that makes neural networks learn. Despite its reputation as a black box, it’s really just the chain rule from calculus applied systematically across a computation graph.</p>

<h2 id="the-core-idea">The Core Idea</h2>

<p>A neural network is a function composed of many simpler functions. Training it means finding weights that minimize a loss function — typically the difference between predicted and actual outputs. Gradient descent does this iteratively: compute how much each weight contributes to the loss, then nudge it in the opposite direction.</p>

<p>Backpropagation is how we compute those gradients efficiently.</p>

<h2 id="a-simple-example">A Simple Example</h2>

<p>Consider a two-layer network with a single training example. The forward pass computes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>z1 = W1 * x + b1
a1 = relu(z1)
z2 = W2 * a1 + b2
loss = (a2 - y)^2
</code></pre></div></div>

<p>The backward pass works in reverse, computing <code class="language-plaintext highlighter-rouge">dLoss/dW</code> for every weight by repeatedly applying the chain rule:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dLoss/dW2 = dLoss/da2 * da2/dz2 * dz2/dW2
</code></pre></div></div>

<p>Each term is easy to compute locally. The key insight is that each layer only needs the gradient flowing in from the layer ahead of it — this is what makes backprop efficient.</p>

<h2 id="why-it-matters">Why It Matters</h2>

<p>Before backpropagation, training deep networks was largely intractable. The 1986 paper by Rumelhart, Hinton, and Williams popularized the algorithm and kicked off the first wave of neural network research. Forty years later, the same fundamental algorithm powers systems with hundreds of billions of parameters.</p>

<p>Understanding it at this level — not just using it via <code class="language-plaintext highlighter-rouge">loss.backward()</code> — changes how you think about network architecture, initialization, and why certain training problems (like vanishing gradients) happen in the first place.</p>

<h2 id="next-steps">Next Steps</h2>

<p>If you want to go deeper, implement a small autograd engine from scratch. Andrej Karpathy’s <a href="https://github.com/karpathy/micrograd">micrograd</a> is an excellent reference — around 150 lines of Python that implement the core of what PyTorch does under the hood.</p>]]></content><author><name>Abhishek Shah</name></author><summary type="html"><![CDATA[A clear walkthrough of backpropagation in neural networks — the chain rule, gradient computation, and why it made deep learning possible.]]></summary></entry></feed>