Learning pathsPricingChecking sign-in…
← LLD problem library

LLD-007 · LOW LEVEL DESIGN

Splitwise / Expense Sharing

Preserve exact totals, immutable history and retry identity while computing shared obligations.

Local Java 21 reference verified: 320 checks. Learner drafts are not executed. Author self-review; independent review pending.

Existing introductory lesson · Version 1

Understand the problem

Design an expense-sharing group that records expenses, exact allocations, weighted/equal splits, settlements and reversals. Explain why this is an obligation ledger rather than a real payment system, and how deterministic rounding and append-only corrections protect auditability.

Before you start

  • Integer money and rounding
  • Value equality and immutable records
  • Atomic compound updates
  • Ledger/projection distinction

Scope and explicit decisions

  • One in-memory group, one implicit currency and registered member IDs. It records obligations only; it does not execute bank transfers, exchange currencies, authenticate members or persist data.
  • Positive balance means the member should receive money; negative means they owe money. Every entry delta sums to zero. Expenses may allocate zero shares but must sum exactly to the positive total.
  • split accepts positive integer weights. Equal split uses weight 1; percentage-like ratios use integer weights such as 50:30:20. No floating-point percentages or fractional currency units are stored.
  • Weighted shares are floored exactly with BigInteger intermediates. Remaining minor units go in lexicographic member-ID order. This is a deterministic rule, not a claim that it is the only fair allocation.
  • A settlement must go from a currently negative member to a currently positive member and cannot exceed either net obligation. It is a global-netting model, not a stored pairwise debt graph.
  • Corrections append inverse entries, including when a prior settlement already occurred. Reversing an expense does not erase its settlement; the remaining balances may reverse who owes whom.
  • A command key is scoped to this ledger. Typed immutable fingerprints compare the actual operation arguments; reused keys with different requests fail. Matching repeats return the original Entry.
  • Every updated long balance is checked before publishing state. Overflow fails without partial ledger mutation. History and dedupe records grow for the process lifetime; durable audit storage is a Staff extension.

Worked examples

Input: A pays 100 shared equally among A, B and C.

Outcome: Shares A34/B33/C33; net A+66/B-33/C-33.

The payer’s own consumption offsets part of the amount they paid.

Input: B settles 33 to A, then the original expense is reversed.

Outcome: After reversal A-33/B+33/C0; original expense, settlement and inverse all remain.

A correction changes obligations; it cannot erase a completed external exchange.

Try before revealing

Hint 1

Write the exact sum of shares and the sign convention first.

Hint 2

How does a payer’s own share change their credit?

Hint 3

Keep every committed fact; use a new inverse entry for correction.

Requirements and invariants

R1 — Weighted allocations conserve the original integer amount. split protects I1.

R2 — Each atomic expense or settlement preserves zero total obligations. expense protects I2.

R3 — Corrections preserve history through one inverse entry. reverse protects I3.

R4 — A retry cannot post the same logical expense twice. expense protects I3.

I1: Every accepted share is nonnegative and the exact sum equals the expense amount.

I2: Each entry delta and all net balances sum to zero; balances equal ordered replay of retained entries.

I3: An operation key resolves one fingerprint/result, and an original entry has at most one accepted reversal.

Derive the design

Calculate balances from history before maintaining a projection

The simplest auditable model stores exact expense/settlement entries and recomputes balances by scanning history. It is correct but each query grows with history. The reference retains append-only entries plus a materialized balance map for quick reads, with rebuild as a verification/recovery calculation. Correctness depends on committing the entry and updated projection together.

Alternative: Pairwise debts preserve who originally owes whom but complicate global netting and simplification. This chapter explicitly chooses group net obligations. Suggested settlement edges are a separate view; a greedy matching algorithm must not be advertised as globally minimizing transaction count without proof.

Define balance sign and settlement eligibility

Without a sign convention, tests can accidentally “prove” the reverse result. State that positive receives and negative owes, and define whether global netting or pairwise debt is the business model.

Separate split calculation from posting

A pure split function can be checked against conservation independently of ledger mutation. Exact shares are then validated again at the command boundary.

Prepare all checked arithmetic before publishing

Compute updated balances into a temporary map. An arithmetic error must not leave a new Entry without its matching projection or command result.

Treat corrections as new facts

Original history explains what happened. Reversal adds another fact; it does not pretend related later settlements disappeared.

Responsibilities and ownership

split

Owns: Pure weighted allocation and remainder rule

Methods: split(amount, weights)

Returns immutable exact shares without posting ledger state.

Ledger

Owns: Members, entries, balances, command identities and reversal index

Methods: expense, settle, reverse, rebuild

One monitor validates and commits a balanced posting.

Entry

Owns: Stable ID, immutable member deltas and optional original reference

Methods: Record accessors

Corrections never mutate this prior entry.

Typed fingerprints

Owns: Normalized operation arguments

Methods: Record equality

Structured identity avoids ambiguity from delimiter-concatenated strings.

Expense posting boundary

One local monitor protects the posting. This is an obligation ledger, not a bank payment API.

  1. Exact shares + command key
  2. ↓ Check registered members and total

    Validate balanced delta
  3. ↓ Prepare all checked balance adds

    Atomic entry + projection
  4. ↓ Store typed retry fingerprint

    Immutable result
Audit correction

A reversal may change who owes whom because prior settlements remain part of history.

  1. Original expense entry
  2. ↓ Retain original fact

    Later settlement entry
  3. ↓ Append correction, do not erase settlement

    Inverse expense entry
  4. ↓ Replay all entries in order

    Rebuilt net balances

Data model, schema and relationships

An expense has a payer and several shares. A settlement is a separate money fact; net balances are a projection of both, scoped to one currency.

Implementation boundary: Expense/share/balance records map to the reference arithmetic. Durable accounts, membership permissions and settlement rails remain adapters.

These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.

member: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

member relationshipsmember 1 to expense 0..*: pays. expense.payer_id → member.id. member 1 to share 0..*: owes shares. share.member_id → member.id. member 1 to settlement 0..*: sends. settlement.from_id → member.id.pays1 parent · 0..* childrenexpenseid [PK]group_id [FK]payer_id [FK]owes shares1 parent · 0..* childrenshareexpense_id + member_id [PK]amountsends1 parent · 0..* childrensettlementid [PK]group_id [FK]from_id [FK]memberid [PK]name
member: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

member relationshipsmember 1 to settlement 0..*: receives. settlement.to_id → member.id.receives1 parent · 0..* childrensettlementid [PK]group_id [FK]from_id [FK]memberid [PK]name
expense_group: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

expense_group relationshipsexpense_group 1 to expense 0..*: contains expenses. expense.group_id → expense_group.id. expense_group 1 to settlement 0..*: records settlements. settlement.group_id → expense_group.id.contains expenses1 parent · 0..* childrenexpenseid [PK]group_id [FK]payer_id [FK]records settlements1 parent · 0..* childrensettlementid [PK]group_id [FK]from_id [FK]expense_groupid [PK]currency
expense: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

expense relationshipsexpense 1 to share 0..*: splits into. share.expense_id → expense.id.splits into1 parent · 0..* childrenshareexpense_id + member_id [PK]amountexpenseid [PK]group_id [FK]payer_id [FK]

Entity definitions and field contracts

member

Identity used in group arithmetic.

FieldType / keyMeaning and reference
iduuid · PKMember identity
nametext · VALUEDisplay label

Code mapping: Member identity.

  • Identity must not depend on mutable display name.

expense_group

Currency and membership scope.

FieldType / keyMeaning and reference
iduuid · PKGroup identity
currencytext · VALUESingle declared currency

Code mapping: Group scope.

  • Do not net mixed currencies without explicit conversion ledger.

expense

One payer-funded expense.

FieldType / keyMeaning and reference
iduuid · PKExpense identity
group_iduuid · FKAccounting scope · expense_group.id
payer_iduuid · FKPayer · member.id
amountbigint · VALUEExact minor units
created_attimestamp · VALUEImmutable acceptance instant for history queries

Code mapping: Expense record.

  • Shares sum exactly to amount; retry identity binds full split request.

share

Participant liability.

FieldType / keyMeaning and reference
expense_iduuid · PK,FKExpense owner · expense.id
member_iduuid · PK,FKOwing participant · member.id
amountbigint · VALUEExact share

Code mapping: Split result.

  • PK(expense_id,member_id); nonnegative amounts and deterministic remainder rule.

settlement

Accepted transfer between members.

FieldType / keyMeaning and reference
iduuid · PKSettlement identity
group_iduuid · FKCurrency scope · expense_group.id
from_iduuid · FKSender · member.id
to_iduuid · FKRecipient · member.id
amountbigint · VALUETransfer amount

Code mapping: Settlement input/projection.

  • Sender differs from recipient; deduplicate settlement identity.

Relationship cardinalities and their reasons

Parent → childMultiplicityFK / why
expense_group → expense

contains expenses

1 parent per child; 0..* children per parentexpense.group_id → expense_group.id

Group determines the single-currency accounting boundary.

member → expense

pays

1 parent per child; 0..* children per parentexpense.payer_id → member.id

Payer can also owe a share; those are distinct roles.

expense → share

splits into

1 parent per child; 0..* children per parentshare.expense_id → expense.id

Join rows resolve expense/member many-to-many liability.

member → share

owes shares

1 parent per child; 0..* children per parentshare.member_id → member.id

Repeated participation across expenses creates net liability.

expense_group → settlement

records settlements

1 parent per child; 0..* children per parentsettlement.group_id → expense_group.id

Settlements change balances without rewriting old expenses.

member → settlement

sends

1 parent per child; 0..* children per parentsettlement.from_id → member.id

Directional foreign key identifies sender.

member → settlement

receives

1 parent per child; 0..* children per parentsettlement.to_id → member.id

A second role-specific foreign key identifies recipient.

Transaction boundaries

  1. Expense insertion, validated shares and balance projection update commit together.
  2. Settlement identity and both balance deltas commit together; simplification suggestions are not actual payment evidence.

Indexes and uniqueness

  1. Index expense(group_id,created_at), share(member_id,expense_id), settlement(group_id).
  2. Unique operation identity with immutable payer/amount/split fingerprint.

Schema traps and failure cases

  1. Rounding each share independently can make shares sum to more than the expense.
  2. A simplified debt graph is not proof of an executed settlement or permission to rewrite history.

Trace the records through one operation

  1. A pays 100; shares A34,B33,C33 conserve the amount.
  2. B pays A 33 under settlement S.
  3. Repeated S changes nothing; projection still sums to zero.

Method contracts

split(amount, positiveWeights) → immutable exactShares

Use exact integer arithmetic and deterministic remainder allocation; reject empty/zero/negative weights.

expense(payer, amount, exactShares, key) → Entry

Validate member IDs and exact sum. Payer gains amount and each member loses their share; a payer share offsets their credit. Compute all balance changes before commit.

settle(debtor, creditor, amount, key) → Entry

Resolve duplicates before checking current eligibility, since the successful original call changed balances. Reject over-settlement and same-member transfers.

reverse(entryId,key) → Entry

Append exact inverse of an original, once. A reversal cannot itself be reversed through this API; original records stay unchanged.

balances(), entries(), rebuild()

Return immutable observations. rebuild replays entries in order using checked arithmetic and should equal the materialized balance projection.

What, when, why and how

Each concept is attached to the requirement it protects, its implementation, an alternative and a test.

Exact allocation and deterministic remainder

what
Represent shares in integer minor units and assign indivisible leftovers explicitly.
when
Amounts do not divide evenly or weights are large.
why
Binary floating point or independent rounding can create or lose a currency unit.
how
Compute floor(amount×weight/sumWeights) using BigInteger; distribute leftover units in sorted member order.

Code: split · Invariants: I1

Alternatives, cost and failure example

Alternative: Largest-remainder allocation is another policy, but requires tie rules and independent tests.

Cost: Sorting P members is O(P log P); exact multiplication/division has bit-length cost. Input amounts/weights are long-sized.

Counterexample: 100 divided three ways rounded to 33 each loses one unit.

Test: Assert 34/33/33, huge weights and exact conservation for seeded varied amounts.

Balanced ledger and materialized projection

what
Retain immutable zero-sum postings while deriving a fast net-balance view.
when
Users need auditability and repeated balance queries.
why
Mutable totals alone cannot explain how a correction or settlement changed obligations.
how
apply verifies zero-sum deltas, computes a temporary next map with checked adds, then commits projection, Entry and command result under one monitor.

Code: apply, rebuild · Invariants: I2

Alternatives, cost and failure example

Alternative: Recompute on each read for smaller histories; add a projection only when its cost and repair contract are justified.

Cost: This implementation copies all M member balances per posting, so the update is O(M+P), not O(P) alone.

Counterexample: Entry is stored, then balance update overflows halfway, leaving history and projection inconsistent.

Test: Force long overflow and verify neither entries nor balances change; compare rebuild after every seeded expense.

Idempotent operation identity

what
Match retries by structured business arguments and retain the original result.
when
A caller loses the response after a commit.
why
A retry can otherwise double the obligation, while reusing one key for another payer can hide data errors.
how
ExpenseFingerprint, SettlementFingerprint and ReversalFingerprint are immutable values. Matching keys return the original entry; mismatches fail. Settlement checks retries before new balance constraints.

Code: Command, ExpenseFingerprint, settle · Invariants: I3

Alternatives, cost and failure example

Alternative: A durable unique key/result row in the same transaction is the persisted equivalent.

Cost: Dedupe retention grows; expiration must preserve the business retry horizon.

Counterexample: A successful settlement is retried, but current debt is now zero and naïve eligibility validation rejects the original result lookup.

Test: Race two identical expense keys; assert one stored entry and identical returned results.

Reversal instead of history rewriting

what
Correct an earlier posting by appending its inverse.
when
An expense was wrong after other events already occurred.
why
Deleting history can silently delete evidence and produce unexplained balances after settlement.
how
reverse references the original, negates each delta with checked arithmetic and records one reversal. All subsequent entries remain.

Code: reverse, Entry · Invariants: I2, I3

Alternatives, cost and failure example

Alternative: Editing an uncommitted draft is simpler; after posting, corrections need an explicit audit model.

Cost: Reversal may produce newly owed balances because actual prior settlements remain recorded.

Counterexample: Erase an expense and its related settlement as if a bank transfer never happened.

Test: Reverse the original after B settled A; rebuild shows A now owes B while preserving the settlement entry.

Read and run the Java reference

Reveal complete Solution.java

Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.

Solution.java Java 21
import java.math.BigInteger;
import java.util.*;
/** One-currency in-memory obligation ledger. This records debts, not real money transfers. */
public final class Solution {
    private Solution() {}
    public record Entry(String id,Map<String,Long> delta,String reverses) { public Entry { delta=Map.copyOf(delta); } }
    private record Command(Object fingerprint,String entryId){}
    private record ExpenseFingerprint(String payer,long amount,Map<String,Long> shares){public ExpenseFingerprint{shares=Map.copyOf(shares);}}
    private record SettlementFingerprint(String debtor,String creditor,long amount){}
    private record ReversalFingerprint(String entryId){}
    public static Map<String,Long> split(long amount,Map<String,Long> weights){
        if(amount<=0||weights.isEmpty()||weights.values().stream().anyMatch(v->v==null||v<=0))throw new IllegalArgumentException("split");
        BigInteger sum=weights.values().stream().map(BigInteger::valueOf).reduce(BigInteger.ZERO,BigInteger::add);
        var shares=new TreeMap<String,Long>();long allocated=0;
        for(var e:new TreeMap<>(weights).entrySet()){
            if(e.getKey()==null||e.getKey().isBlank())throw new IllegalArgumentException("participant");
            long share=BigInteger.valueOf(amount).multiply(BigInteger.valueOf(e.getValue())).divide(sum).longValueExact();shares.put(e.getKey(),share);allocated=Math.addExact(allocated,share);
        }
        // Each floor loses less than one unit: leftover is strictly less than participant count.
        long remainder=amount-allocated;for(String id:shares.keySet()){if(remainder--<=0)break;shares.put(id,shares.get(id)+1);}
        return Map.copyOf(shares);
    }
    public static final class Ledger {
        private final Set<String> members;
        private final Map<String,Long> balances=new TreeMap<>();
        private final Map<String,Entry> entries=new LinkedHashMap<>();
        private final Map<String,Command> commands=new HashMap<>();
        private final Map<String,String> reversed=new HashMap<>();
        private long sequence;
        public Ledger(Set<String> members){if(members.isEmpty()||members.stream().anyMatch(s->s==null||s.isBlank()))throw new IllegalArgumentException("members");this.members=Set.copyOf(members);members.forEach(m->balances.put(m,0L));}
        private void member(String id){if(!members.contains(id))throw new IllegalArgumentException("member");}
        private static String key(String value){if(value==null||value.isBlank())throw new IllegalArgumentException("key");return value;}
        private Entry apply(String commandKey,Object fingerprint,Map<String,Long> delta,String reverseId){
            key(commandKey);var prior=commands.get(commandKey);if(prior!=null){if(!prior.fingerprint().equals(fingerprint))throw new IllegalArgumentException("key reused");return entries.get(prior.entryId());}
            BigInteger sum=delta.values().stream().map(BigInteger::valueOf).reduce(BigInteger.ZERO,BigInteger::add);if(sum.signum()!=0)throw new IllegalArgumentException("unbalanced");
            var next=new TreeMap<>(balances);delta.forEach((m,value)->{member(m);next.put(m,Math.addExact(next.get(m),value));});
            String id="E"+(++sequence);var entry=new Entry(id,delta,reverseId);balances.clear();balances.putAll(next);entries.put(id,entry);commands.put(commandKey,new Command(fingerprint,id));return entry;
        }
        public synchronized Entry expense(String payer,long amount,Map<String,Long> exactShares,String commandKey){
            member(payer);if(amount<=0||exactShares.isEmpty()||exactShares.values().stream().anyMatch(v->v==null||v<0))throw new IllegalArgumentException("shares");
            exactShares.keySet().forEach(this::member);
            BigInteger sum=exactShares.values().stream().map(BigInteger::valueOf).reduce(BigInteger.ZERO,BigInteger::add);if(!sum.equals(BigInteger.valueOf(amount)))throw new IllegalArgumentException("sum");
            var shares=new TreeMap<>(exactShares);var delta=new TreeMap<String,Long>();shares.forEach((m,v)->delta.put(m,-v));delta.merge(payer,amount,Math::addExact);
            Object fingerprint=new ExpenseFingerprint(payer,amount,shares);
            return apply(commandKey,fingerprint,delta,"");
        }
        public synchronized Entry settle(String debtor,String creditor,long amount,String commandKey){
            member(debtor);member(creditor);if(debtor.equals(creditor)||amount<=0)throw new IllegalArgumentException("settlement");
            Object fingerprint=new SettlementFingerprint(debtor,creditor,amount);
            // Resolve repeats before balance validation, since successful settlement changes eligibility.
            var prior=commands.get(key(commandKey));if(prior!=null){if(!prior.fingerprint().equals(fingerprint))throw new IllegalArgumentException("key reused");return entries.get(prior.entryId());}
            if(balances.get(debtor)>=0||balances.get(creditor)<=0||BigInteger.valueOf(balances.get(debtor)).negate().compareTo(BigInteger.valueOf(amount))<0||balances.get(creditor)<amount)throw new IllegalStateException("over settlement");
            return apply(commandKey,fingerprint,Map.of(debtor,amount,creditor,-amount),"");
        }
        public synchronized Entry reverse(String entryId,String commandKey){
            var original=entries.get(entryId);if(original==null||!original.reverses().isEmpty())throw new IllegalArgumentException("original entry");
            Object fingerprint=new ReversalFingerprint(entryId);var prior=commands.get(key(commandKey));if(prior!=null){if(!prior.fingerprint().equals(fingerprint))throw new IllegalArgumentException("key reused");return entries.get(prior.entryId());}
            if(reversed.containsKey(entryId))throw new IllegalStateException("already reversed");
            var inverse=new TreeMap<String,Long>();original.delta().forEach((m,v)->inverse.put(m,Math.negateExact(v)));
            var result=apply(commandKey,fingerprint,inverse,entryId);reversed.put(entryId,result.id());return result;
        }
        public synchronized Map<String,Long> balances(){return Map.copyOf(balances);}
        public synchronized List<Entry> entries(){return List.copyOf(entries.values());}
        public synchronized Map<String,Long> rebuild(){var result=new TreeMap<String,Long>();members.forEach(m->result.put(m,0L));for(var e:entries.values())e.delta().forEach((m,v)->result.put(m,Math.addExact(result.get(m),v)));return Map.copyOf(result);}
    }
}

split preserves every minor unit

Floored shares cannot over-allocate. Each participant loses less than one unit to flooring, so total remainder is less than participant count and one sorted pass can assign it.

apply validates then publishes

Balanced sum uses BigInteger so even validation does not overflow. Checked long per-member adds occur in a temporary map before sequence/entry/projection mutation.

settle resolves prior result before eligibility

The first successful settlement changed current balances. Repeat detection must happen first or the retry can appear to over-settle.

reverse preserves causal history

One inverse entry links to its original. It is safe to rebuild because every accepted posting follows the same checked balance transition.

Complexity and limits

  • Weighted splitting sorts P participant IDs: O(P log P), plus bounded-size BigInteger arithmetic. Shares and weights use O(P) space.
  • Posting copies M balances, validates P deltas and stores an entry: O(M+P) expected work plus typed fingerprint maps. Immutable balance snapshots also copy O(M).
  • rebuild scans all retained postings E with total delta count D: O(M+D). History/dedupe/reversal retention grows O(D+E).
  • The monitor serializes all postings. Real large-group storage would use a transactional ledger and carefully versioned projections; a concurrent map does not make a multi-member posting atomic.

Walk through the actual reference

These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.

Exact split and settlement

Step 1 of 3: Split 100 equally among A, B, C

Shares 34,33,33; A paid

Invariant: Shares sum exactly to 100

Read the complete trace table
ActionState after actionInvariant
Split 100 equally among A, B, CShares 34,33,33; A paidShares sum exactly to 100
Post expenseBalances A +66, B -33, C -33Net obligations sum to zero
B settles 33 to AA +33, B 0, C -33Settlement records obligation change, not a real bank transfer

Correction after settlement

Step 1 of 2: Reverse original expense

Append inverse entry; original remains

Invariant: Audit history is append-only

Read the complete trace table
ActionState after actionInvariant
Reverse original expenseAppend inverse entry; original remainsAudit history is append-only
Rebuild balancesA -33, B +33, C 0Prior settlement is not erased by expense correction

Mistakes and counterexamples

Round every share independently

Failing case: 33+33+33 for amount100.

Repair: Use exact units and an explicit remainder policy.

Maintain totals without entries

Failing case: A disputed correction has no reproducible explanation.

Repair: Retain immutable postings and a rebuildable projection.

Deduplicate by unescaped concatenated strings

Failing case: Member identifiers contain separators used by the fingerprint format.

Repair: Use typed structured equality or canonical length-safe serialization.

Claim greedy settlement minimizes all transfers

Failing case: A valid greedy matching need not solve a global combinatorial minimum.

Repair: State whether the objective is validity or optimality and prove/test accordingly.

Engineer → Senior → Staff

Choose a deep dive

Implement exact splits and clear balances.

Executed: weighted/equal allocation, exact expense validation and settlements.

Change exercise

Add a 50:30:20 split of 101 and explain which member receives the leftover unit.

Worked answer guidance

Calculate floor shares 50/30/20 and allocate the remaining one by the declared sorted-ID rule. The answer depends on identity order, so keep that contract visible.

Interview follow-ups

Why are amounts integers?

A currency minor unit is indivisible in this model. Integer/decimal policies with explicit rounding preserve totals; floating point is unnecessary.

Does a positive balance mean money is in a wallet?

No. It means a net group obligation to receive; this ledger performs no actual payment.

Your design attempt

Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.

Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.

Solution.java
Run program · Java 21

No unsaved changes · 208/20000

Self-review

Checks apply to this visit. Self-review is not a passed submission or verified mastery.

Open focused practice →

Provenance and scope of evidence

Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.

Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.