<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ahmed Mahsook]]></title><description><![CDATA[Ahmed Mahsook]]></description><link>https://ahmedmahsook.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Ahmed Mahsook</title><link>https://ahmedmahsook.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 06 Oct 2026 14:34:09 GMT</lastBuildDate><atom:link href="https://ahmedmahsook.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Financially Correct Wallet and Settlement System in Go]]></title><description><![CDATA[When I started building PriceFlow's wallet, I assumed the hard part would be CRUD: add money, subtract money, done. It wasn't. The hard part was defining what "correct" actually means when money is in]]></description><link>https://ahmedmahsook.hashnode.dev/building-a-financially-correct-wallet-and-settlement-system-in-go</link><guid isPermaLink="true">https://ahmedmahsook.hashnode.dev/building-a-financially-correct-wallet-and-settlement-system-in-go</guid><category><![CDATA[golang]]></category><category><![CDATA[fintech]]></category><category><![CDATA[backend developments]]></category><category><![CDATA[distributed system]]></category><dc:creator><![CDATA[Ahmed Mahsook]]></dc:creator><pubDate>Mon, 05 Oct 2026 11:40:10 GMT</pubDate><content:encoded><![CDATA[<p>When I started building PriceFlow's wallet, I assumed the hard part would be CRUD: add money, subtract money, done. It wasn't. The hard part was defining what "correct" actually means when money is involved — and then defending that definition against every way the system could fail.</p>
<p>This is the story of that definition.</p>
<h2>The Problem</h2>
<p>Imagine a user has ₹10,000 in their wallet. They place a ₹4,000 bid. Now you have to represent something your balance column was never designed to express:</p>
<pre><code class="language-plaintext">Available Balance = ₹6,000
Held Balance      = ₹4,000
Total             = ₹10,000
</code></pre>
<p>The ₹4,000 isn't gone. It's reserved — committed to a bid that might win, might lose, might get outbid in the next second. A single <code>balance</code> field can't represent "this money exists, but you can't spend it right now." You need two numbers, and you need an invariant that never breaks:</p>
<pre><code class="language-plaintext">Available + Held = Total wallet funds
</code></pre>
<p>If that invariant ever drifts — if a bug lets <code>Available + Held</code> exceed the user's real funds — you haven't shipped a backend bug. You've created money that doesn't exist. That distinction shaped every decision that followed.</p>
<p><strong>Architecture, roughly:</strong></p>
<pre><code class="language-plaintext">                 ┌──────────────┐
                 │   Bid API    │
                 └──────┬───────┘
                        │
                        ▼
                 ┌──────────────┐
                 │ Wallet Logic │
                 └──────┬───────┘
                        │
             ┌──────────┴──────────┐
             ▼                     ▼
      Available Balance       Held Balance
             │                     │
             └──────────┬──────────┘
                        ▼
                  PostgreSQL
            (transactions + row locking)
                        │
                        ▼
              Settlement / Refund
                 (idempotent)
</code></pre>
<h2>1. Money Is an Integer, Not a Float</h2>
<p>The first decision has nothing to do with auctions — it's about representation. <code>float64</code> cannot represent currency exactly; rounding errors compound silently across thousands of operations until your ledger and your bank statement disagree.</p>
<pre><code class="language-go">// Never this:
amount := 100.50

// Always this — smallest currency unit, as an integer:
amount := int64(10050) // ₹100.50, stored as paise
</code></pre>
<p>The financial layer represents monetary values as <code>int64</code> paise rather than floating-point values, throughout. A 2% platform fee on a ₹10,000 win is <code>1000000 * 2 / 100 = 20000</code> paise — deterministic, exact, reproducible.</p>
<h2>2. Available vs. Held: The Core Invariant</h2>
<p>This is the structural decision everything else depends on. A wallet in PriceFlow isn't one number:</p>
<pre><code class="language-sql">CREATE TABLE wallets (
    user_id UUID PRIMARY KEY,
    available_balance BIGINT NOT NULL DEFAULT 0,
    held_balance       BIGINT NOT NULL DEFAULT 0
);
</code></pre>
<p>When a bid is placed, money moves from <code>available</code> to <code>held</code> — not out of the wallet, just out of reach:</p>
<pre><code class="language-plaintext">Before:  Available ₹10,000  |  Held ₹0
Bid ₹4,000 →
After:   Available  ₹6,000  |  Held ₹4,000
</code></pre>
<p>Nothing left the user's total. The invariant held. This is the number I check first whenever I'm debugging anything wallet-related — if it's ever violated, the bug is upstream of whatever I'm looking at.</p>
<h2>3. Concurrency: The Bug That Doesn't Show Up in Testing</h2>
<p>Here's the failure mode that doesn't appear until real concurrent load hits it. Two bids arrive for the same user at nearly the same instant — a genuine race, or a network retry firing twice. Both read <code>available_balance = 1000</code> <em>before either has written anything</em>. Both see enough funds. Both proceed. Both hold ₹1,000.</p>
<p>The user now has ₹2,000 held against ₹1,000 they actually had. That's a double-spend, and it will never show up in single-user manual testing — only under real concurrent access, which is exactly when you can least afford it.</p>
<p>The fix is a row-level lock, held for the duration of the check-then-write:</p>
<pre><code class="language-go">func (r *WalletRepository) GetWalletForUpdate(ctx context.Context, tx pgx.Tx, userID string) (*Wallet, error) {
    row := tx.QueryRow(ctx, `
        SELECT user_id, available_balance, held_balance
        FROM wallets WHERE user_id = $1
        FOR UPDATE
    `, userID)
    // ...
}
</code></pre>
<p><code>FOR UPDATE</code> forces a second concurrent request for the same user to wait until the first transaction commits or rolls back — at which point it reads the updated balance and correctly sees there isn't enough left. Different users never block each other; only genuine contention on the same row serializes.</p>
<h2>4. The Outbid Scenario — Where It Gets Interesting</h2>
<p>Alice bids ₹4,000. Bob bids ₹5,000. Alice's hold needs to release the instant she's overtaken:</p>
<pre><code class="language-plaintext">Alice before:  Available ₹6,000  |  Held ₹4,000
Bob outbids →
Alice after:   Available ₹10,000 |  Held ₹0
Bob after:     Available ₹5,000  |  Held ₹5,000
</code></pre>
<p>But this release has to be safe against duplication. If the same "release Alice's hold" operation fires twice — a retried event, a duplicate message — you get:</p>
<pre><code class="language-plaintext">Release ₹4,000
Release ₹4,000 (again)
→ Available = ₹14,000
</code></pre>
<p>Money appeared from nowhere. The fix isn't "try not to send it twice" — distributed systems will retry. The fix is designing every financial operation so a retry is a no-op, not a second transaction.</p>
<h2>5. Idempotency: A Retry Must Never Become a Second Transaction</h2>
<p>Every hold, release, and settlement in PriceFlow carries a <code>reference_id</code> tying it to the specific bid or payment it belongs to:</p>
<pre><code class="language-plaintext">Settlement ID: SETTLE-123

First request:  process → transfer money → mark completed
Retry:           SETTLE-123 already processed → do nothing
</code></pre>
<p>The principle: idempotency isn't a nice-to-have for financial operations, it's the baseline requirement. A payment provider's webhook will retry. A network call will time out after the money already moved. The system has to recognize "I've already done this" before it does it again.</p>
<h2>6. Database Transactions: Nothing Financial Happens Halfway</h2>
<p>A single settlement touches at least four things: debit the buyer's hold, credit the seller, record the platform's fee, close the hold. The important part is that these operations are not independent writes — they share the same transaction boundary. If any of them fails independently, you get a state no reconciliation should ever have to explain:</p>
<pre><code class="language-plaintext">Buyer debited ✅
Seller credited ❌
Service crashed
</code></pre>
<p>That's not a bug ticket, that's money that's unaccounted for. So every related financial write happens inside one database transaction, all-or-nothing:</p>
<pre><code class="language-go">dbTx, err := pool.Begin(ctx)
defer dbTx.Rollback(ctx) // no-op once committed

updateBalances(ctx, dbTx, ...)
createLedgerEntry(ctx, dbTx, ...)

return dbTx.Commit(ctx)
</code></pre>
<p>If the ledger insert fails after the balance update succeeded, the whole transaction rolls back — the balance update never happened either, as far as the database is concerned.</p>
<h2>7. The Auction Lifecycle Gates the Money</h2>
<p>PriceFlow's auctions move through <code>SCHEDULED → OPEN → CLOSED → MATCHED</code>. Financial operations aren't allowed to ignore where the auction actually is:</p>
<pre><code class="language-plaintext">OPEN    → bids accepted, holds placed
CLOSED  → no new bids
MATCHED → winner determined, winning hold persists
          → settlement workflow begins
          → financial settlement completes
</code></pre>
<p>A bid that arrives after <code>CLOSED</code>, or a settlement attempt on an auction still <code>OPEN</code>, is a bug the financial layer has to refuse — not because the money math is wrong, but because the timing is wrong. State and money are coupled on purpose.</p>
<h2>8. When a Service Crashes Mid-Transfer</h2>
<p>This is the scenario that actually matters: the process dies between "debit buyer" and "credit seller." What does the system look like on restart?</p>
<p>The answer isn't a clever recovery trick. The database transaction prevents a crash from leaving a partially committed financial update, while idempotency makes a retry safe if the operation is attempted again. They solve different failure modes, and the system needs both: the transaction guarantees nothing is left half-done, and idempotency guarantees that whatever retries afterward doesn't repeat what already succeeded.</p>
<h2>9. Disputes and Refunds Ask the Same Question Twice</h2>
<p>A buyer disputes a delivery. If the dispute is upheld, funds get refunded instead of settled to the seller:</p>
<pre><code class="language-plaintext">Settlement completed → Dispute raised → Dispute accepted → Refund initiated
</code></pre>
<p>The interesting question was never "how do I refund ₹5,000." It was "how do I guarantee ₹5,000 can never be refunded twice" — the same idempotency and transaction-boundary thinking from settlement, applied to the reverse direction of the same money.</p>
<h2>10. What Could Go Wrong — and What Stops It</h2>
<table>
<thead>
<tr>
<th>Failure</th>
<th>Protection</th>
</tr>
</thead>
<tbody><tr>
<td>Duplicate settlement request</td>
<td>Idempotency check on <code>reference_id</code></td>
</tr>
<tr>
<td>Duplicate refund</td>
<td>Idempotency check</td>
</tr>
<tr>
<td>Service crashes mid-transaction</td>
<td>DB transaction, all-or-nothing</td>
</tr>
<tr>
<td>Floating-point rounding</td>
<td>Integer paise, never float</td>
</tr>
<tr>
<td>Hold released twice</td>
<td>Idempotent release, state check</td>
</tr>
<tr>
<td>Settlement at the wrong auction state</td>
<td>Lifecycle validation before any transfer</td>
</tr>
<tr>
<td>Partial financial update</td>
<td>Transaction rollback</td>
</tr>
<tr>
<td>Two concurrent bids race the same balance</td>
<td>Row-level locking (<code>FOR UPDATE</code>)</td>
</tr>
</tbody></table>
<p>That table is the actual content of the article. Everything above it is just the reasoning that produced each row.</p>
<h2>The Real Lesson</h2>
<p>Building the wallet wasn't the hard part. Adding and subtracting integers is not a hard problem.</p>
<p>The hard part was accepting that a financial system isn't correct because the balance looks right while everything runs normally — it's correct only if it stays right when requests are duplicated, services crash mid-write, auctions change state underneath a pending operation, and the same event gets processed twice because a network timed out. I designed each part with failure in mind. If something goes wrong, the invariant should still hold.</p>
<p>That's what I took from building PriceFlow's financial layer: correctness isn't the normal case. It's every abnormal case, handled on purpose.</p>
]]></content:encoded></item><item><title><![CDATA[Idempotency in a Go Wallet System: How I Made Sure Money Never Moves Twice]]></title><description><![CDATA[The problem
In PriceFlow, every bid holds money. When you bid ₹500 on an auction, that ₹500 gets locked in your wallet so you can actually pay if you win. If someone outbids you, your hold gets releas]]></description><link>https://ahmedmahsook.hashnode.dev/idempotency-in-a-go-wallet-system-how-i-made-sure-money-never-moves-twice</link><guid isPermaLink="true">https://ahmedmahsook.hashnode.dev/idempotency-in-a-go-wallet-system-how-i-made-sure-money-never-moves-twice</guid><category><![CDATA[golang]]></category><category><![CDATA[fintech software development]]></category><category><![CDATA[backend developments]]></category><category><![CDATA[System Design]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Ahmed Mahsook]]></dc:creator><pubDate>Thu, 01 Oct 2026 07:34:46 GMT</pubDate><content:encoded><![CDATA[<h2>The problem</h2>
<p>In PriceFlow, every bid holds money. When you bid ₹500 on an auction, that ₹500 gets locked in your wallet so you can actually pay if you win. If someone outbids you, your hold gets released. When the auction closes, the winner's money moves into escrow, and eventually to the seller.</p>
<p>That sounds simple until you ask: what happens if this operation runs twice?</p>
<p>A network retry, a client that times out and resubmits, two goroutines racing on the same bid — any of these can trigger the same wallet operation more than once. If "hold ₹500" runs twice by accident, you've locked ₹1,000 instead of ₹500. If "release hold" runs twice, you might release money that should still be protected. In a system handling real money, this isn't a theoretical edge case — it's the first thing that has to be correct, before anything else.</p>
<h2>Why floats were never an option</h2>
<p>Before getting into idempotency, one smaller but equally non-negotiable decision: money in PriceFlow is stored as integers — paise, not rupees, and never as a float. Floating-point arithmetic has rounding behavior that's fine for most things and completely unacceptable for money. Storing everything as an integer count of the smallest currency unit sidesteps the entire category of bug.</p>
<h2>Making operations idempotent</h2>
<p>The core guarantee doesn't live in application code — it lives in the database. Every wallet transaction is inserted with a <code>reference_id</code> tied to the real-world event that caused it (a specific bid, a specific refund), paired with the transaction <code>type</code>:</p>
<pre><code class="language-go">func (r *WalletRepository) CreateTransaction(
    ctx context.Context,
    tx *WalletTransaction,
) error {
    query := `
        INSERT INTO wallet_transactions
            (id, user_id, type, amount, reference_id, sequence_id, created_at)
        VALUES
            ($1, $2, $3, $4, $5, DEFAULT, NOW())
        ON CONFLICT (reference_id, type)
        DO NOTHING
    `

    _, err := r.db.Exec(
        ctx, query,
        tx.ID, tx.UserID, tx.Type, tx.Amount, tx.ReferenceID,
    )

    return err
}
</code></pre>
<p>That <code>ON CONFLICT (reference_id, type) DO NOTHING</code> is doing the real work, backed by a unique constraint on exactly those two columns:</p>
<pre><code class="language-sql">CREATE UNIQUE INDEX idx_wallet_transactions_reference_type
ON wallet_transactions(reference_id, type);
</code></pre>
<p>If <code>ReleaseBidHold</code> for a given bid gets called twice — because of a retry, a timeout, or a race — the second insert simply doesn't happen. The database rejects the duplicate before it can double-release or double-charge anything. This is deliberately a database-level guarantee rather than an application-level check like "query first, then insert if not found" — a check-then-insert in application code has its own race window between the check and the insert; a unique constraint closes that window entirely, because the database itself is the single source of truth for "has this already happened."</p>
<h2>Hold and release under concurrent bidding</h2>
<p>The flow when a bidder gets outbid:</p>
<pre><code class="language-go">func (s *WalletService) ReleaseBidHold(
    ctx context.Context,
    userID string,
    bidID string,
    amount int64,
) error {
    referenceID := "bid:" + bidID

    if err := s.repo.ReleaseHold(ctx, userID, amount); err != nil {
        return err
    }

    return s.repo.CreateTransaction(ctx, &amp;WalletTransaction{
        UserID:      userID,
        Type:        TransactionTypeHoldRelease,
        Amount:      amount,
        ReferenceID: referenceID,
    })
}
</code></pre>
<p>End to end, the sequence looks like this:</p>
<pre><code class="language-plaintext">New bidder places higher bid
        ↓
Wallet.Hold(new bidder)
        ↓
Previous bidder becomes outbid
        ↓
Wallet.Release(previous bidder)
        ↓
HOLD_RELEASE transaction recorded
</code></pre>
<p>The <code>reference_id</code> of <code>"bid:" + bidID</code> means that no matter how many times a release gets triggered for that specific bid — a retry from the caller, a duplicate event — only one <code>HOLD_RELEASE</code> transaction is ever recorded for it. The balance can't drift from repeated calls, which matters most exactly when load is highest: several bidders competing on the same auction, each triggering holds and releases in quick succession.</p>
<h2>Escrow: protecting money across a multi-day window</h2>
<p>Once an auction closes, the winner's held funds move into escrow rather than being released. From there, the money stays protected through shipping, delivery, and a protection period, before a settlement step pays the seller and deducts the platform fee.</p>
<p>This is where the same <code>reference_id</code> + <code>type</code> pattern matters most, at a different point in the system. A settlement worker runs periodically to find escrows whose protection period has expired and pay them out. If that worker crashes mid-run and restarts, it can't be allowed to pay the same seller twice for the same escrow — the exact same uniqueness guarantee that protects a bid hold from being released twice is what protects a seller payout from being issued twice.</p>
<h2>What I'd do differently</h2>
<p>I'd apply this same idempotency pattern as a deliberate first decision for every new financial operation in the system, rather than reasoning about it case by case as each one came up. Once you've seen how cleanly a unique constraint closes the race that a check-then-insert leaves open, there's no reason not to make it the default for anything touching money from day one.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Crash-Safe Bid Engine in Go: Heaps, Race Conditions, and Recovery]]></title><description><![CDATA[The problem
PriceFlow lets sellers create auctions and buyers compete through real-time bids. The engine behind this needs to do three things well: handle many bids arriving close together, always kno]]></description><link>https://ahmedmahsook.hashnode.dev/building-a-crash-safe-bid-engine-in-go-heaps-race-conditions-and-recovery</link><guid isPermaLink="true">https://ahmedmahsook.hashnode.dev/building-a-crash-safe-bid-engine-in-go-heaps-race-conditions-and-recovery</guid><category><![CDATA[golang]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[backend developments]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[System Design]]></category><dc:creator><![CDATA[Ahmed Mahsook]]></dc:creator><pubDate>Tue, 29 Sep 2026 06:19:20 GMT</pubDate><content:encoded><![CDATA[<h3>The problem</h3>
<p>PriceFlow lets sellers create auctions and buyers compete through real-time bids. The engine behind this needs to do three things well: handle many bids arriving close together, always know the current highest bid instantly, and — because that state lives in memory for speed — not lose it if the service restarts.</p>
<p>The first two are a data structures problem. The third turned out to be where most of the real engineering happened.</p>
<h3>Designing the bid book: a heap plus a hashmap</h3>
<p>For each auction, I needed fast answers to two different questions, and no single data structure answers both well.</p>
<p><strong>Question one: who's currently winning?</strong> A max-heap keeps the highest-priority bid at the top at all times, so checking the current leader is O(1), and inserting a new bid is O(log n) — no re-sorting a list every time a bid comes in.</p>
<p>Priority isn't just price, though. Two bids can land with the exact same price, and under real concurrent load, they can even share the exact same timestamp (clock resolution has limits). Without a tiebreaker that's always unique, the heap has no correct answer for which one goes first. So priority resolves in three steps:</p>
<p>go</p>
<pre><code class="language-go">func (b *BidBook) Less(i, j int) bool {
    if b.bids[i].Price != b.bids[j].Price {
        return b.bids[i].Price &gt; b.bids[j].Price       // higher price wins
    }
    if !b.bids[i].SubmittedAt.Equal(b.bids[j].SubmittedAt) {
        return b.bids[i].SubmittedAt.Before(b.bids[j].SubmittedAt) // earlier wins
    }
    return b.bids[i].SequenceNo &lt; b.bids[j].SequenceNo  // final, always-unique tiebreaker
}
</code></pre>
<p><code>SequenceNo</code> is a counter, incremented atomically on every bid, so there's always exactly one right answer, even if price and timestamp both tie.</p>
<p><strong>Question two: find this exact bid, fast.</strong> Say a bidder wants to cancel. The heap is great at "who's winning" but terrible at "find bid #4821" — that's an O(n) scan through the whole structure. So alongside the heap, I keep a hashmap (<code>byID</code>) mapping bid ID to a pointer to that same <code>Bid</code> struct. It's not a copy of the data — both the heap slice and the hashmap point at the identical objects in memory, kept in sync every time a bid is added or removed.</p>
<h3>The concurrency bug that taught me the most</h3>
<p>Go's <code>container/heap</code> package doesn't just call <code>Push</code> and <code>Pop</code> — a single heap operation from outside can trigger internal calls to <code>Len</code>, <code>Less</code>, and <code>Swap</code> too, as part of re-sorting. My first instinct was to lock only <code>Push</code> and <code>Pop</code>, since those are the "obvious" writes. That leaves a window: if <code>Less</code> or <code>Swap</code> run unprotected while another goroutine is mid-write, that's still a race, just through a different door. All five methods needed the same lock.</p>
<p>That decision avoided one class of bug. It caused another.</p>
<p>When I added bid cancellation, I needed a way to remove <em>any</em> bid from the heap, not just the top one — so each <code>Bid</code> stores its own current position (<code>index</code>), updated every time <code>Swap</code> runs. Cancellation became: look up the bid by ID, get its index, call <code>heap.Remove</code> at that position.</p>
<p>I wrote <code>Remove()</code> to lock the mutex first, then call <code>heap.Remove()</code>. That looked correct. It wasn't. Internally, <code>heap.Remove()</code> re-sorts the heap by calling back into my own <code>Less</code> and <code>Swap</code> methods — and each of those <em>also</em> tries to acquire the same mutex, because I'd locked all five methods individually. Go's <code>sync.Mutex</code> isn't reentrant: a goroutine can't lock a mutex it's already holding. So the moment I called cancel, the program froze — permanently, with no error, no panic, just silence.</p>
<p>I found this by testing through a real client, not through a static analysis tool — I placed a bid, cancelled it, and watched it hang instead of returning. The fix was removing the outer lock from <code>Remove()</code> itself; the individual heap methods it calls already lock correctly on their own, so wrapping the whole thing in another lock was both redundant and, in this case, fatal.</p>
<p>That bug is the one I'd tell an interviewer about first. It's not a bug you can spot by reading the code once you have to actually trace who's calling what, and understand that "more locking" isn't automatically "more correct."</p>
<h3>Surviving a restart: the Replay Engine</h3>
<p>The heap and hashmap live in memory because that's what makes them fast. It also means a crash or restart wipes every active auction's bid state — unless something durable exists to rebuild from.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abb50a38938337e6e150b4d/43d33778-681b-491f-a6e0-723a3aacb246.png" alt="" style="display:block;margin:0 auto" />

<p>The approach: every domain event (right now, <code>BidPlaced</code>) gets persisted as it happens, through a separate Persistence Service talking over gRPC. On startup, a Replay Engine reads the saved events in order and re-applies them, rebuilding the heap and hashmap from scratch. Because ordering is fully deterministic price, then time, then sequence number replaying the same events always produces the same state back.</p>
<p>Building this surfaced two bugs that are easy to miss if you're not specifically looking for them.</p>
<p><strong>Bug one: replay silently reassigned sequence numbers.</strong> My <code>Push</code> method always handed out a <em>new</em> sequence number from the atomic counter — correct for a brand-new bid, wrong during replay. A bid that originally held ticket #5 could come back with a completely different number, scrambling the tie-order that made the auction fair in the first place.</p>
<p><strong>Bug two: the counter didn't catch up after replay.</strong> Even with bug one fixed, a freshly restarted <code>BidBook</code> starts its sequence counter at zero. If replay restores 50 old bids with numbers up to #50, the next genuinely new bid after restart would also get handed #1 colliding with a number that already exists. The fix for both:</p>
<p>go</p>
<pre><code class="language-go">func ReplayBidBook(itemID string, events []BidPlacedEvent) *BidBook {
    bb := NewBidBook(itemID)
    var maxSeq int64

    for _, e := range events {
        bid := e.ToBid()          // keeps the ORIGINAL SequenceNo, doesn't reassign
        heap.Push(bb, bid)
        if bid.SequenceNo &gt; maxSeq {
            maxSeq = bid.SequenceNo
        }
    }

    bb.seqCounter = maxSeq        // new bids after restart continue counting up from here
    return bb
}
</code></pre>
<p>Both were confirmed with a test: feed in three fake bid-placed events, then check that the rebuilt bid book has the right count, the right bid on top, and a counter correctly caught up to the highest number replayed.</p>
<p><strong>What's still open:</strong> this proves the replay <em>logic</em> is correct against a known set of events. It doesn't yet prove the full loop capturing real events live as bids come in, persisting them to Postgres, and recovering an actual running service after a real crash. That's the next piece of work, not something I'm claiming is done.</p>
<h3>What I'd do differently</h3>
<p>I'd write the concurrency tests for <code>Remove()</code> before adding it, not after — the deadlock would have surfaced in a test run instead of a hung client. I'd also design the locking strategy for the whole heap interface up front, as one decision, rather than arriving at "lock all five methods" one bug at a time. It worked out, but it was slower than it needed to be.</p>
]]></content:encoded></item></channel></rss>