Machine Coding Problem

Digital Wallet

maco30maco60macoAllfintechcommand-patterndeadlock-preventiontransaction-rollbackacid
Commonly Asked By:PayPalBlockStripeCoinbase

Requirements & System Scope

Functional Scope (In-Scope)

  • Atomic Money Transfers: Perform money transactions securely ensuring complete atomic credit and debit steps.
  • Zero Floating-Point Errors: Use arbitrary-precision decimals (BigDecimal) to avoid float calculation flaws.
  • Strict Idempotency: Check idempotency keys before transaction processing to block duplicates.
  • Command Pattern for Rollbacks: Encapsulate operations inside Command objects, supporting robust undo/rollback mechanisms.
  • Append-Only Event Ledger: Maintain immutable transactional histories.

Explicit Boundaries (Out-of-Scope)

  • No Real Bank Card Integration: Bypasses live Visa/Mastercard processing flows or clearinghouses.
  • No Multi-Currency FX Engine: Overwrite parameters do not calculate real-time FX exchange conversion spreads.

Class Diagram & Entity Relationships

Structural layout showing relationships between wallets, transaction commands, and the wallet coordinator service:

Loading...
  • Command Pattern Representation: Transactions are modeled as command objects, maintaining internal states (PENDING, SUCCESS, FAILED, ROLLED_BACK) and referencing target wallets directly.
  • Atomic Account Protection: Wallets contain individual ReentrantLocks to support high-performance thread safety.

Design Patterns & SOLID Principles

  • Command Pattern (Transaction Orchestration): Decoupling execution from transaction requests. The TransactionCommand interface structures actions cleanly, keeping business logic (P2P transfers, deposits, withdrawals) separate from the core service class. It also enables self-contained rollback behaviors.
  • Deterministic Lock Sorting Pattern (Deadlock Prevention): Instead of locking source then destination (which triggers deadlocks if two parties pay each other simultaneously), locks are acquired in sorted string order of their unique wallet IDs.
  • Single Responsibility Principle (SRP):Wallet handles balance adjustments; TransactionCommand manages the concrete atomic transfer execution; WalletService manages registration, ledger auditing, and rollback triggers.

Core Execution Workflows

P2P Transfer Sequence

  1. Sender requests transfer: executeTransfer(idempotencyKey, srcId, destId, amount).
  2. Verify transaction registry:
    1. If key exists, return cached transaction record immediately.
  3. Sort wallet IDs alphabetically to avoid deadlock.
  4. Acquire first lock, then acquire second lock:
    1. Verify source wallet balance has sufficient funds.
    2. Deduct funds from source and credit destination wallet.
    3. Set transaction status to SUCCESS.
  5. On failure, set status to FAILED (rollback state is preserved naturally as balances are not modified).
  6. Write command to append-only ledger list, release locks, and return transaction details.

Concurrency & Thread Safety Strategy

The critical concurrency vulnerability occurs when multiple transfer threads attempt to read and write to the same account:

  • Deterministic Sorted Locks: Standardizing the lock order by wallet ID prevents cyclical dependency deadlocks.
  • Concurrent HashMaps: Store active wallets and idempotency logs in concurrent structures to prevent data corruption.
  • Fine-Grained Reentrant Locks: Account-level locks maximize transaction throughput rather than throttling at service-wide levels.

Complete Clean Code Blueprint

Thread-safe, command-pattern-driven financial transfer blueprints in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

enum TransactionStatus { PENDING, SUCCESS, FAILED, ROLLED_BACK }

interface TransactionCommand {
    boolean execute();
    void undo();
    String getId();
    TransactionStatus getStatus();
    BigDecimal getAmount();
}

class Wallet {
    private final String id;
    private BigDecimal balance;
    private final ReentrantLock lock = new ReentrantLock();

    public Wallet(String id, BigDecimal initialBalance) {
        this.id = id;
        this.balance = initialBalance;
    }

    public String getId() { return id; }
    
    public BigDecimal getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }
    }

    public ReentrantLock getLock() { return lock; }

    public boolean debit(BigDecimal amount) {
        if (balance.compareTo(amount) >= 0) {
            balance = balance.subtract(amount);
            return true;
        }
        return false;
    }

    public void credit(BigDecimal amount) {
        balance = balance.add(amount);
    }
}

class P2PTransferCommand implements TransactionCommand {
    private final String id;
    private final Wallet source;
    private final Wallet destination;
    private final BigDecimal amount;
    private TransactionStatus status;

    public P2PTransferCommand(String id, Wallet source, Wallet destination, BigDecimal amount) {
        this.id = id;
        this.source = source;
        this.destination = destination;
        this.amount = amount;
        this.status = TransactionStatus.PENDING;
    }

    @Override
    public boolean execute() {
        // Sort locks alphabetically by wallet ID to ensure deadlock immunity
        Wallet first = source.getId().compareTo(destination.getId()) < 0 ? source : destination;
        Wallet second = source.getId().compareTo(destination.getId()) < 0 ? destination : source;

        first.getLock().lock();
        try {
            second.getLock().lock();
            try {
                if (source.debit(amount)) {
                    destination.credit(amount);
                    status = TransactionStatus.SUCCESS;
                    return true;
                } else {
                    status = TransactionStatus.FAILED;
                    return false;
                }
            } finally {
                second.getLock().unlock();
            }
        } finally {
            first.getLock().unlock();
        }
    }

    @Override
    public void undo() {
        if (status != TransactionStatus.SUCCESS) return;

        Wallet first = source.getId().compareTo(destination.getId()) < 0 ? source : destination;
        Wallet second = source.getId().compareTo(destination.getId()) < 0 ? destination : source;

        first.getLock().lock();
        try {
            second.getLock().lock();
            try {
                if (destination.debit(amount)) {
                    source.credit(amount);
                    status = TransactionStatus.ROLLED_BACK;
                }
            } finally {
                second.getLock().unlock();
            }
        } finally {
            first.getLock().unlock();
        }
    }

    public String getId() { return id; }
    public TransactionStatus getStatus() { return status; }
    public BigDecimal getAmount() { return amount; }
}

class WalletService {
    private final Map<String, Wallet> wallets = new ConcurrentHashMap<>();
    private final Map<String, TransactionCommand> idempotencyStore = new ConcurrentHashMap<>();
    private final List<TransactionCommand> ledger = new CopyOnWriteArrayList<>();

    public void registerWallet(Wallet wallet) {
        wallets.put(wallet.getId(), wallet);
    }

    public Wallet getWallet(String id) {
        return wallets.get(id);
    }

    public TransactionCommand executeTransfer(String idempotencyKey, String srcId, String destId, BigDecimal amount) {
        if (idempotencyStore.containsKey(idempotencyKey)) {
            System.out.println("[Service] Returning cached transaction for idempotency key: " + idempotencyKey);
            return idempotencyStore.get(idempotencyKey);
        }

        Wallet src = wallets.get(srcId);
        Wallet dest = wallets.get(destId);
        if (src == null || dest == null) {
            throw new IllegalArgumentException("Invalid wallets involved in transfer");
        }

        String txnId = "TXN-" + UUID.randomUUID().toString().substring(0, 8);
        TransactionCommand command = new P2PTransferCommand(txnId, src, dest, amount);

        command.execute();
        ledger.add(command);
        idempotencyStore.put(idempotencyKey, command);

        return command;
    }

    public void rollbackTransaction(String txnId) {
        for (TransactionCommand cmd : ledger) {
            if (cmd.getId().equals(txnId)) {
                cmd.undo();
                System.out.println("[Service] Successfully rolled back Transaction: " + txnId);
                return;
            }
        }
        System.out.println("[Service] Transaction not found for rollback: " + txnId);
    }

    public List<TransactionCommand> getLedger() {
        return Collections.unmodifiableList(ledger);
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== Digital Wallet Concurrent Simulation ===");
        WalletService service = new WalletService();

        Wallet w1 = new Wallet("ACC-01", new BigDecimal("1000.00"));
        Wallet w2 = new Wallet("ACC-02", new BigDecimal("500.00"));
        service.registerWallet(w1);
        service.registerWallet(w2);

        ExecutorService executor = Executors.newFixedThreadPool(2);

        // Concurrent overlapping payments between ACC-01 and ACC-02 (locks are ordered, preventing deadlocks)
        executor.submit(() -> {
            service.executeTransfer("KEY-101", "ACC-01", "ACC-02", new BigDecimal("200.00"));
        });

        executor.submit(() -> {
            service.executeTransfer("KEY-102", "ACC-02", "ACC-01", new BigDecimal("100.00"));
        });

        executor.shutdown();
        executor.awaitTermination(2, TimeUnit.SECONDS);

        System.out.println("ACC-01 Balance: " + w1.getBalance());
        System.out.println("ACC-02 Balance: " + w2.getBalance());

        // Perform a rollback simulation
        TransactionCommand lastTxn = service.getLedger().get(0);
        System.out.println("\nRolling back transaction: " + lastTxn.getId() + " of amount: " + lastTxn.getAmount());
        service.rollbackTransaction(lastTxn.getId());

        System.out.println("ACC-01 Balance after rollback: " + w1.getBalance());
        System.out.println("ACC-02 Balance after rollback: " + w2.getBalance());
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...