Coding Interview Workshop · Lesson 20

Bound duplicate-request decisions honestly

An idempotency key is bound to a specific request meaning, not merely remembered as seen.

Clarify the observable contract

An idempotency key is bound to a specific request meaning, not merely remembered as seen. This bounded in-memory ledger accepts a new key and amount, replays an equal request, rejects a changed amount and rejects new keys once capacity is full. Previously accepted requests remain replayable at capacity. The task is a single-owner decision model with no external side effect or persistence.

Lookup precedes capacity admission because an already known key owns an existing decision. Repeating it needs no new slot. Payload equality is part of identity: the same key with another amount is a conflict even if there is otherwise room. The example uses an integer amount as its entire bounded request meaning.

Walk through the verified program

x with amount ten is accepted, then repeated as replay. x with eleven conflicts with the saved amount. y fills the second entry and z cannot be admitted. Lookup happens before the capacity check, so a repeat does not require another slot. The printed outcomes come from actual dictionary state, not a mocked network service.

The third x request does not overwrite ten with eleven, so later replay still refers to the original accepted amount. After y is accepted, rejecting z leaves the dictionary unchanged. This ordering makes rejection nonmutating and keeps subsequent behavior predictable under the stated single-owner assumption.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

var ledger=new Ledger(2);
Console.WriteLine(ledger.Accept("x",10)); Console.WriteLine(ledger.Accept("x",10)); Console.WriteLine(ledger.Accept("x",11));
Console.WriteLine(ledger.Accept("y",20)); Console.WriteLine(ledger.Accept("z",30));
sealed class Ledger(int capacity) {
    private readonly Dictionary<string,int> values=new(StringComparer.Ordinal);
    public string Accept(string key,int amount) {
        if(string.IsNullOrEmpty(key) || amount<0 || capacity<1) return "invalid";
        if(values.TryGetValue(key,out int previous)) return previous==amount?"replay":"conflict";
        if(values.Count==capacity) return "full";
        values.Add(key,amount); return "accepted";
    }
}

Account for time, storage and allocations

Expected lookup and insertion cost are O(1) dictionary operations plus key hashing. Retained entry count is bounded by capacity, but key lengths and payload representation still affect memory. There is no eviction policy; dropping old keys would change the period over which retries can be recognized.

An entry-count cap can coexist with arbitrarily long key strings unless key length is bounded separately. The amount is fixed-size here, but storing a complete result or request fingerprint can alter memory. Retention and eviction also determine how long a retry can be recognized, making them correctness policies as well as resource choices.

Review assumptions and likely defects

This local dictionary is not thread-safe and cannot provide distributed exactly-once effects. Accepting a record before or after an external payment leaves a failure window unless storage and effect ownership are designed together. A key reused with changed payload must not silently replay an unrelated result.

A concurrent dictionary would make individual operations safe but would not automatically make lookup, capacity checking and insertion one atomic policy transition. A real shared implementation needs synchronization or a storage uniqueness constraint around the compound decision. Crash consistency and external-effect ownership remain separate from in-process thread safety.

Practice one: discriminate edge cases

Replay x after the ledger is full and verify replay rather than full. Hint: capacity limits new ownership, not lookup of already owned decisions. Add invalid empty-key and negative-amount cases.

Repeat both the original x amount and its conflicting amount after filling capacity. Hint: neither should be classified as full because both refer to an already stored key, but one is replay and the other conflict. This catches implementations that place the size check before identity and payload validation.

Practice two: defend an extension

Design a durable multi-replica operation with request fingerprinting and an explicit retention horizon. Identify transaction boundaries, in-progress states and crash recovery, while refusing to claim that a dictionary or a local file makes arbitrary external effects exactly once.

A durable design can store a unique scoped key, a canonical request fingerprint, an in-progress or completed state and the recorded response. It must define who executes an effect after a crash and which transaction can bind that effect to the record. When an external system cannot participate, an explicit reconciliation or outbox-style boundary is needed instead of an exactly-once slogan.

Evaluate correctness and interview reasoning

The interview explanation should trace all five outcomes and identify the nonmutating branches. It should state that capacity limits new decisions and that a key is bound to payload meaning. A senior answer then distinguishes entry bounds, thread safety, durable state and external-effect semantics, which require different mechanisms. The console dictionary demonstrates none of the latter distributed guarantees. This task is valuable precisely because a correct tiny state machine can expose those boundaries clearly, giving reviewers a concrete counterexample to designs that remember only seen keys or reject every retry once a cache is full.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

var ledger=new Ledger(2);
Console.WriteLine(ledger.Accept("x",10)); Console.WriteLine(ledger.Accept("x",10)); Console.WriteLine(ledger.Accept("x",11));
Console.WriteLine(ledger.Accept("y",20)); Console.WriteLine(ledger.Accept("z",30));
sealed class Ledger(int capacity) {
    private readonly Dictionary<string,int> values=new(StringComparer.Ordinal);
    public string Accept(string key,int amount) {
        if(string.IsNullOrEmpty(key) || amount<0 || capacity<1) return "invalid";
        if(values.TryGetValue(key,out int previous)) return previous==amount?"replay":"conflict";
        if(values.Count==capacity) return "full";
        values.Add(key,amount); return "accepted";
    }
}

Expected output

accepted
replay
conflict
accepted
full
Try this
Replay x after the ledger is full and verify replay rather than full. Hint: capacity limits new ownership, not lookup of already owned decisions. Add invalid empty-key and negative-amount cases.

Further reading

Interview questions for this lesson