Escrow Service Low-Level Design: Fund Holding, Release Conditions, and Dispute Resolution

Escrow Model

An escrow service acts as a neutral third party: the buyer transfers funds to a platform-controlled escrow account, the seller fulfills an obligation, and the platform releases funds to the seller on confirmation. If either party disputes the outcome, an arbiter resolves the claim. This model is used in marketplaces, freelance platforms, and real estate transactions.

Escrow Transaction Schema

  • escrow_id — unique identifier (UUID)
  • buyer_wallet_id, seller_wallet_id — parties to the transaction
  • amount, currency — held amount in smallest unit
  • status — state machine: PENDINGFUNDEDCONDITION_METRELEASINGRELEASED or DISPUTEDREFUNDED
  • conditions[] — list of release condition objects (type + parameters)
  • created_at, expires_at — creation time and expiry deadline

Funding

The buyer initiates funding by transferring the escrow amount from their wallet to the platform's escrow wallet. This uses the same atomic transfer mechanism as the wallet service. On successful transfer, the escrow record moves to status FUNDED and both parties are notified.

Release Conditions

Conditions are typed and stored as JSON alongside the escrow record:

  • manual_confirmation — buyer explicitly confirms receipt via API call
  • automatic_timer — system releases funds N days after FUNDED if no dispute is raised
  • milestone_completion — third-party system (project management tool) signals milestone done
  • third_party_confirmation — logistics provider confirms delivery via webhook

A condition evaluator service polls or receives webhooks and marks conditions met. When all required conditions are satisfied, the escrow advances to CONDITION_MET.

Release Flow

On condition met:

  1. Compute platform fee (deducted from escrow amount)
  2. Transfer amount - fee from escrow wallet to seller wallet (atomic)
  3. Transfer fee from escrow wallet to platform fee wallet
  4. Update escrow status to RELEASED
  5. Notify buyer and seller

The release step is idempotent — a unique constraint on the release transaction prevents double-release under retries.

Dispute Workflow

Either party can raise a dispute while the escrow is in FUNDED or CONDITION_MET status:

  1. Escrow status → DISPUTED; funds remain frozen in escrow wallet
  2. An arbiter is assigned (support agent or automated rule engine)
  3. Evidence submission period: both parties upload supporting documents via signed S3 URLs
  4. Arbiter issues a decision: full release to seller, full refund to buyer, or partial split
  5. System executes the decision as one or two atomic transfers

All arbiter actions are logged with actor ID, timestamp, and decision rationale for compliance.

Partial Release for Milestones

For project-based escrows, the full contract amount is held upfront and released incrementally:

  • Escrow record stores a milestones[] array with percentage and condition per milestone
  • On each milestone completion, amount * milestone_pct is released to the seller
  • Remaining funds stay locked until subsequent milestones complete
  • Dispute can be raised against any unreleased milestone independently

Expiry Handling

A scheduler checks for escrows where expires_at < NOW() and status is still FUNDED. These are auto-refunded to the buyer. Expiry prevents funds from being locked indefinitely if the seller abandons the transaction.

Multi-Currency and FX Rate Locking

When the escrow is created, the FX rate at that moment is stored on the escrow record. All subsequent calculations (fees, partial releases, dispute splits) use this locked rate, not the spot rate at time of release. This protects both parties from currency fluctuation during the escrow period.

Audit Trail

Every escrow state transition is written to an append-only audit log: (escrow_id, from_status, to_status, actor_id, timestamp, reason). This log is immutable and retained for the compliance window (typically 7 years). Regulators can reconstruct the full lifecycle of any transaction from this log alone.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you model escrow fund-holding so that money is neither accessible to buyer nor seller until release conditions are met?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Create a dedicated escrow ledger account per transaction. When a buyer initiates escrow, debit their wallet and credit the escrow account — funds are now in a state of HELD. The escrow account is owned by the platform, not either party, so neither can withdraw from it directly. The escrow record in the database carries a state machine: CREATED -> FUNDED -> RELEASED or REFUNDED or DISPUTED. Release conditions (delivery confirmation, time-based expiry, multisig approval) are evaluated by the escrow service before issuing a debit from escrow and a credit to the seller. All state transitions are logged immutably with timestamps and the actor who triggered them.”
}
},
{
“@type”: “Question”,
“name”: “What release conditions should an escrow service support, and how do you implement time-based automatic release?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Common release conditions include: explicit buyer confirmation, delivery webhook from a logistics provider, multisig approval from both parties, and time-based auto-release after a no-dispute window. For time-based release, store a release_at timestamp on the escrow record and run a periodic job (cron or a queue consumer reading a scheduled-events table) that queries for escrows where release_at <= NOW() AND status = FUNDED. The job transitions each record to RELEASED and enqueues a payout to the seller. Make this job idempotent by using a database-level status check-and-update in a single atomic UPDATE … WHERE status = 'FUNDED' statement, so concurrent job workers don't double-release."
}
},
{
"@type": "Question",
"name": "How would you design the dispute resolution flow for an escrow service, including escalation to human review?",
"acceptedAnswer": {
"@type": "Answer",
"text": "When a buyer raises a dispute, transition the escrow to DISPUTED and freeze it — neither auto-release nor seller withdrawal can proceed. Store a dispute record linked to the escrow with the claimant ID, reason code, supporting evidence URLs, and a deadline for the other party to respond. Implement tiered resolution: first offer a negotiation window where both parties can propose a split (e.g., 80% seller / 20% refund); if unresolved within N days, escalate to an internal agent queue. Agents see the escrow state, transaction history, and uploaded evidence. Their decision is recorded with an agent ID, ruling rationale, and timestamp, and the system executes the ruling by debiting the escrow account and crediting the appropriate parties. Maintain a full audit trail for regulatory compliance."
}
},
{
"@type": "Question",
"name": "How do you ensure escrow funds are safe against system failures mid-transaction — for example, a crash between debit and credit?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use a write-ahead log (WAL) or an outbox pattern. Before performing any escrow state change, write the intended operation to an outbox table in the same database transaction as the state update. A separate relay process reads the outbox and executes downstream effects (crediting the seller, firing webhooks) with at-least-once delivery semantics. Each outbox event carries an idempotency key so replaying it is safe. For cross-service fund moves, adopt a saga: each step publishes a domain event; compensating transactions undo previous steps if a later step fails. Use a saga orchestrator or choreography log to track which steps completed, enabling recovery after a crash without orphaned funds sitting in limbo indefinitely."
}
}
]
}

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Coinbase Interview Guide

See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering

Scroll to Top