Designing a Marketplace Dispute Resolution System

Designing a Marketplace Dispute Resolution System

Designing a Marketplace Dispute Resolution System

A complete, beginner-to-advanced walkthrough of the least glamorous but most trust-critical system in any marketplace — evidence submission, case tracking, automated resolution, immutable audit trails, and safe money movement — built to gracefully survive a scenario of a million requests per minute during flash sales and post-holiday return spikes.

01

Introduction & History

Every marketplace that connects strangers to trade goods or services — eBay, Amazon, Etsy, Airbnb, Upwork, Flipkart — eventually runs into the same hard problem: what happens when a buyer and a seller disagree? The buyer says the shoes never arrived. The seller says they shipped them. The buyer says the laptop was “like new” but arrived scratched. Someone has to decide who is right, and someone has to move money, reputation, or goods accordingly.

This is the job of a Dispute Resolution System (DRS). It is one of the least glamorous but most business-critical systems inside any marketplace, because it directly controls trust. If disputes are resolved slowly, unfairly, or inconsistently, buyers stop buying and sellers stop selling. Everything else the marketplace has built — search, recommendations, checkout, logistics — runs downstream of that trust.

Real-life analogy

Think of a dispute resolution system like a small claims court built into a shopping mall. If you buy a toaster from a mall vendor and it’s broken, you don’t hire a lawyer and sue — you go to a mall help desk, show your receipt and photos of the broken toaster, the desk checks the vendor’s side of the story, and within a day or two a decision is made: refund, replacement, or “sorry, this isn’t covered.” The dispute resolution system is that help desk, except it runs entirely in software, for millions of “toasters” a day, across dozens of currencies and jurisdictions.

1.1 A short history

In the early days of e-commerce (late 1990s), disputes were handled almost entirely by humans: email support agents reading complaints and manually deciding refunds. This didn’t scale. As marketplaces like eBay grew into millions of daily transactions, companies built dedicated “Trust & Safety” or “Resolution Center” platforms — software that could log disputes, attach evidence, and route cases to the right queue.

Over the 2010s, three big shifts happened:

  • Self-service case creation — buyers and sellers could open a dispute themselves through an app instead of calling support.
  • Structured evidence capture — instead of free-text emails, systems started asking for photos, tracking numbers, and structured reason codes, making cases easier to compare, aggregate, and automate.
  • Automated / rules-based resolution — for simple, repetitive cases (for example, “package marked delivered but buyer says not received, and it’s under $20”), machine logic started resolving cases in seconds instead of routing everything to a human agent.

Modern systems (2020s onward) combine rules engines, machine learning risk scoring, and human review queues into a hybrid pipeline — which is exactly the kind of system this tutorial will design from scratch. The point of walking through the historical arc first is that each layer of today’s architecture is not arbitrary; it was added specifically to solve a pain that its predecessor generation left behind, and knowing which pain each layer answers is what lets you make sensible choices when you inevitably have to trim scope for a smaller marketplace.

02

Problem & Motivation

Let’s be precise about what we’re solving. A marketplace dispute resolution system must let a buyer or seller do all of the following, reliably and at scale.

  • Open a case against an order (“item not received,” “item not as described,” “seller not responding,” “payment not received,” etc.).
  • Attach evidence — photos, videos, PDFs, chat logs, tracking numbers.
  • Track the status of the case in real time.
  • Communicate with the other party and/or a mediator inside the case thread.
  • Receive a fair, explainable resolution — automatically for simple cases, or via a human agent for complex ones.
💬
Why this is hard

A dispute resolution system isn’t just a CRUD app for support tickets. It has to move money (refunds, holds, payouts), it has to be provably fair (regulators and courts can ask for the reasoning behind a decision), it has to resist fraud (buyers falsely claiming “not received” to get free goods, sellers colluding with fake buyer accounts), and at marketplace scale it has to process an enormous, bursty volume of evidence uploads and status checks — all while never losing a single case’s audit trail.

2.1 Business goals

Goal

Speed

Resolve straightforward cases in seconds to minutes, not days, to protect buyer trust and seller cash flow. A refund that arrives a week later feels less like customer service and more like an apology, no matter how correct the decision itself is.

Goal

Fairness & auditability

Every decision must be explainable, reversible on appeal, and logged for compliance. If a regulator or an appeals team asks “why did you refund this?” six months later, the answer must be a specific rule name and the exact fact values that triggered it — never “the algorithm decided.”

Goal

Fraud resistance

Detect abuse patterns (serial “item not received” claimants, self-dealing sellers, coordinated rings) without punishing legitimate buyers who happen to have had a run of bad luck with a shipping carrier.

Goal

Scale

Support huge bursts — flash sales, holiday seasons, viral product returns — where dispute volume can spike violently and unpredictably above baseline, then subside back within days.

03

Core Concepts

Before any architecture diagram, we need shared vocabulary. Every design decision later reduces to a choice between these primitives, and interviewers will happily probe any of them.

3.1 Dispute / Case

A case is the central object: it represents one disagreement tied to one order between a buyer and a seller. It has a lifecycle (open → evidence collection → under review → resolved → closed → possibly appealed → re-opened).

Beginner example

Priya buys a phone case online. It arrives broken. She opens a dispute case, choosing the reason “item damaged,” and uploads two photos. That single action creates exactly one case record in the system, tied to exactly one order, with a status of OPEN and an evidence-collection deadline seven days out.

3.2 Evidence

Evidence is any file or structured data submitted to support a party’s claim — photos, videos, shipment tracking data, chat transcripts, invoices. Evidence must be stored immutably (never silently edited) once submitted, because it may later be reviewed by a human agent, an appeals team, or in rare cases a real court.

3.3 Reason Codes

Instead of free text, most systems ask the buyer/seller to pick from a fixed list of reason codes (for example, ITEM_NOT_RECEIVED, ITEM_NOT_AS_DESCRIBED, DAMAGED_IN_TRANSIT, WRONG_ITEM, SELLER_UNRESPONSIVE). This is what makes automation possible — you can’t write a rule against unlimited free text, but you can write a rule against “reason = ITEM_NOT_RECEIVED AND order_value < $25 AND tracking shows delivered AND buyer has < 2 prior disputes this quarter.”

3.4 Resolution

The outcome of a case: full refund, partial refund, replacement, no action, or account penalty. Resolutions can be automated (a rules engine or ML model decides) or manual (a human trust and safety agent decides).

3.5 SLA (Service Level Agreement)

An internal promise about how fast a case type must be handled — for example, “auto-resolvable cases must decide within 30 seconds; agent-reviewed cases must get a first response within 24 hours.” SLAs are the metric that turns fairness from an abstract goal into an operational one that on-call engineers can page on.

3.6 Escrow / Payment Hold

Many marketplaces don’t release a seller’s payout the instant an order ships — they hold funds for a window (or until delivery confirmation) precisely so that if a dispute is opened, there is money available to refund without having to claw it back from a seller who may have already withdrawn it. The dispute resolution system needs to know, for every case, whether funds are still held or have already been released, because that changes which resolution actions are even possible.

Beginner example

Imagine a piggy bank that keeps a seller’s money for seven days after a sale before letting them take it out. If a buyer complains on day three, the money is still sitting in the piggy bank, so giving it back is instant. If the complaint comes on day ten, the system has to go get the money back from the seller instead, which is slower and riskier.

3.7 Mediation vs. Arbitration

Mediation is when the system (or a human agent) simply facilitates buyer and seller talking to each other and reaching their own agreement — the platform doesn’t impose a decision. Arbitration is when the platform itself renders a binding decision because the two parties can’t agree, or because the case type doesn’t allow direct negotiation (for example, a safety complaint). Our case state machine supports both: a case can sit in a “negotiation” sub-state where only messaging happens, and only escalates to arbitration (automated or agent-driven) if negotiation times out or either party requests it.

3.8 Chargeback

A chargeback happens when a buyer disputes a charge directly with their bank or card network instead of through the marketplace’s own dispute system. This is important architecturally because a chargeback can arrive from an external system (a payment processor webhook) at any time, even for an order that already has, or never had, an internal case — so the Case Service must be able to create or link a case from an external trigger, not only from a buyer clicking “open a dispute” in the app.

3.9 Reputation / Trust Score

A rolling score attached to each buyer and seller account reflecting dispute history, on-time shipping, and resolution outcomes. This score feeds the ML risk model (section 9) and is also surfaced to the human agent console so an agent reviewing a case immediately sees whether they’re dealing with a long-trusted seller or a brand-new, high-risk account.

💬
What an interviewer may ask
  • How would you design the reason-code taxonomy so it stays extensible as new dispute types emerge?
  • What’s the difference between a “case” and a “ticket” in your data model, and why does that distinction matter?
  • How do you handle a dispute that spans multiple items in one order — one case per line item, or one umbrella case?
04

Requirements

Requirements come in two flavors: what the system must let users do (functional), and how well it must do it under stress (non-functional). Both matter equally in a design interview.

4.1 Functional requirements

  • Buyer/seller can open a dispute against an eligible order.
  • Both parties can upload evidence (images, video, PDF, text) tied to a case.
  • Both parties can view real-time case status and timeline.
  • System auto-resolves eligible simple cases using rules plus risk scoring.
  • Complex cases route to a human agent queue with all evidence pre-summarized.
  • Either party can appeal a resolution once.
  • Admins/agents can search, filter, and bulk-manage cases.
  • Full audit trail per case (who did what, when, and why).

4.2 Non-functional requirements

RequirementTarget
Availability99.99% (~52 min downtime/year) for case creation and evidence upload
Latency (case create)p99 < 300 ms
Latency (auto-resolution decision)p99 < 2 s
Throughput (peak)Up to 1,000,000 requests/minute (~16,700 RPS) across read + write APIs during flash sales / post-holiday return spikes
DurabilityZero evidence loss; 11-nines object storage durability
ConsistencyStrong consistency on case-state transitions; eventual consistency acceptable for analytics/search indexes
ComplianceImmutable audit logs, data residency per region, GDPR “right to access”
Sizing intuition

A million requests per minute sounds huge, but it’s about 16,700 requests every second on average — comparable to the traffic a large airline or food-delivery app sees during a flash sale. The design below is built so no single component has to handle that whole number directly; instead, it’s spread across stateless services, queues, and caches, the same way an airport spreads a million passengers a day across many check-in counters instead of one giant desk.

99.99%availability target
1M+requests / minute peak
<300msp99 case-create latency
11 9sevidence durability
05

Architecture & Components

Below is the high-level architecture. Every box names the actual component involved (not just a generic layer), because at this scale the specific technology choice at each hop is part of the design, not an implementation detail to defer.

flowchart TD U[“Buyer / Seller Client App
Web + Mobile”] –> DNS[“DNS + GeoDNS
Route 53 / Cloudflare”] DNS –> CDN[“CDN Edge Cache
CloudFront / Akamai”] CDN –> WAF[“WAF + DDoS Shield
AWS Shield / Cloudflare WAF”] WAF –> GLB[“Global L4 Load Balancer
AWS NLB / GSLB”] GLB –> APIGW[“API Gateway
Kong / AWS API Gateway
AuthN, Rate Limiting, Routing”] APIGW –> RLB[“Regional L7 Load Balancer
ALB / Envoy”] RLB –> CASESVC[“Case Service
Stateless Java Microservice”] RLB –> EVSVC[“Evidence Service
Stateless Java Microservice”] RLB –> RESSVC[“Resolution Engine Service
Rules + ML Scoring”] RLB –> NOTIFYSVC[“Notification Service
Push / Email / SMS”] CASESVC –> CACHE[“Distributed Cache
Redis Cluster”] CASESVC –> QUEUE1[“Kafka Topic
case-events”] EVSVC –> OBJSTORE[“Object Storage
S3 / GCS”] EVSVC –> QUEUE2[“Kafka Topic
evidence-events”] QUEUE1 –> RESSVC QUEUE2 –> SCANSVC[“Malware / Content Scan
Async Worker Pool”] SCANSVC –> OBJSTORE RESSVC –> RULESDB[“Rules Config Store
Postgres + Cache”] RESSVC –> MLINFER[“ML Risk Scoring Service
Model Server, TorchServe”] RESSVC –> QUEUE3[“Kafka Topic
resolution-events”] QUEUE3 –> LEDGERSVC[“Payments / Ledger Service
Refund + Payout Engine”] QUEUE3 –> NOTIFYSVC QUEUE3 –> AGENTQ[“Human Agent Queue Service
Elasticsearch Backed”] CASESVC –> CASEDB[“Primary DB
Sharded PostgreSQL”] LEDGERSVC –> LEDGERDB[“Ledger DB
PostgreSQL, Strong Consistency”] AGENTQ –> SEARCHIDX[“Search Index
Elasticsearch Cluster”] CASEDB –> CDC[“Change Data Capture
Debezium”] CDC –> QUEUE4[“Kafka Topic
db-changes”] QUEUE4 –> SEARCHIDX QUEUE4 –> ANALYTICS[“Analytics Warehouse
Snowflake / BigQuery”]
Figure 5.1 — End-to-end dispute resolution architecture with edge, gateway, services, storage, queues, and downstream consumers

5.1 Component-by-component explanation

CDN + WAF (edge layer)

Before a request even reaches our data centers, static assets (JS, CSS, evidence-upload UI) are served from CDN edge nodes, and a Web Application Firewall blocks obviously malicious traffic (SQL injection attempts, known bad IPs, volumetric DDoS). This is the first scale lever: it removes a large fraction of the “million requests a minute” from ever hitting application servers.

API Gateway

The API Gateway is the single front door for all client traffic. It does authentication (validating JWT/OAuth tokens), coarse-grained rate limiting per user/IP, request routing to the right backend service, and request/response logging. Centralizing this avoids every microservice re-implementing auth and throttling.

📌
Software example

A buyer’s app calls POST /v1/cases with a bearer token. The API Gateway verifies the token’s signature and expiry, checks the buyer hasn’t exceeded “10 case creations per hour” rate limit, then forwards the request to the Case Service with the verified user ID injected as a header.

Load Balancers (Global L4 + Regional L7)

We use two layers: a global Layer-4 load balancer (or GSLB/GeoDNS) to route users to their nearest healthy region, and a regional Layer-7 load balancer to distribute traffic across service instances based on HTTP-level routing (path, headers). L7 also enables canary releases — routing 5% of traffic to a new version of the Case Service.

Case Service

A stateless Java microservice owning the case lifecycle state machine: create case, transition status, attach party comments, and enforce eligibility rules (for example, “you can only dispute an order within 60 days of delivery”). Being stateless means we can horizontally scale it to hundreds of pods behind Kubernetes without any sticky-session complexity.

Evidence Service

Handles evidence uploads. It doesn’t store files in a database — it generates a pre-signed upload URL so the client uploads large files (photos/videos) directly to object storage, bypassing our application servers entirely for the heavy bytes. This is critical for scale: our servers only handle small JSON metadata requests, not gigabytes of video.

Resolution Engine Service

The brain of automation. It combines a rules engine (deterministic, explainable if-this-then-that logic) with an ML risk-scoring model (probabilistic fraud/behavior signal) to decide: auto-resolve now, or route to a human agent.

Message Queues (Kafka)

Kafka decouples every stage of the pipeline. When a case is created, the Case Service doesn’t call the Resolution Engine directly (a synchronous call chain would collapse under load) — it publishes a case-created event and returns immediately. The Resolution Engine consumes events at its own pace, which absorbs traffic spikes.

Ledger Service

A separate service purely for money movement (refunds, holds, payouts), backed by a strongly-consistent database, because financial correctness cannot be eventually consistent. It’s isolated from the rest of the system so a bug in, say, the notification service can never touch money.

Human Agent Queue + Search Index

Cases that fail auto-resolution land in a prioritized queue for trust and safety agents, backed by Elasticsearch so agents can search or filter thousands of open cases by reason code, order value, region, or SLA breach risk.

💬
What an interviewer may ask
  • Why generate pre-signed URLs instead of routing evidence uploads through your application servers?
  • Why is the Ledger Service a separate service instead of a table inside the Case Service’s database?
  • Where would you put a circuit breaker in this architecture, and why?
06

Internal Working

Let’s trace exactly what happens, component by component, when a buyer opens a dispute — this is the sequence an interviewer usually wants you to narrate on a whiteboard.

sequenceDiagram participant C as Client App participant GW as API Gateway participant CS as Case Service participant Q as Kafka case-events participant RE as Resolution Engine participant CACHE as Redis Cache participant DB as Sharded PostgreSQL C->>GW: POST /v1/cases with reason code and order id GW->>GW: Validate JWT and rate limit GW->>CS: Forward request CS->>DB: Check order eligibility DB–>>CS: Order found, eligible CS->>DB: Insert case row, status OPEN CS->>CACHE: Write case summary to cache CS->>Q: Publish case-created event CS–>>GW: Return 201 with case id GW–>>C: Return case id and status Q–>>RE: Consume case-created event RE->>RE: Evaluate rules plus risk score RE->>DB: Update case status RE->>Q: Publish resolution-decided event
Figure 6.1 — End-to-end sequence when a buyer opens a new dispute case

Notice that the client gets an immediate response (case created) long before the resolution logic even runs. This asynchronous hand-off is what lets the write path stay fast under a million-requests-a-minute load: the Case Service’s job is small (validate, persist, publish), and the heavier decision logic happens off the request path.

6.1 Case state machine

stateDiagram-v2 [*] –> OPEN OPEN –> EVIDENCE_COLLECTION EVIDENCE_COLLECTION –> UNDER_AUTO_REVIEW UNDER_AUTO_REVIEW –> AUTO_RESOLVED UNDER_AUTO_REVIEW –> ESCALATED_TO_AGENT ESCALATED_TO_AGENT –> AGENT_RESOLVED AUTO_RESOLVED –> APPEALED AGENT_RESOLVED –> APPEALED APPEALED –> ESCALATED_TO_AGENT AUTO_RESOLVED –> CLOSED AGENT_RESOLVED –> CLOSED CLOSED –> [*]
Figure 6.2 — Case state machine with automated review, escalation, appeal, and closure paths

Every arrow above is a guarded transition. For example, OPEN → EVIDENCE_COLLECTION only fires once at least one evidence item is attached or a timer expires, and APPEALED → ESCALATED_TO_AGENT only fires once per case, since each party gets exactly one appeal, enforced at the service layer, not just in the UI.

💬
What an interviewer may ask
  • What guarantees does the client have about ordering if two events, such as a case update and an evidence upload, race each other?
  • How would you prevent a case from getting stuck in UNDER_AUTO_REVIEW forever if the Resolution Engine crashes mid-processing?
07

Data Flow & Case Lifecycle

A dispute case moves through predictable phases. Understanding the data flow at each phase tells you which storage system and which consistency model to use.

7.1 Phase 1: Creation

Buyer submits reason code and order ID. Case Service validates order eligibility (order exists, is within the dispute window, hasn’t already got an open case) against the order database, then writes a new row to the sharded case table with status OPEN. This is a synchronous, strongly consistent write — the buyer must see their case exists immediately after creation.

7.2 Phase 2: Evidence Collection

Both parties can attach evidence for a configurable window (for example, 48 hours). Evidence bytes go straight to object storage via pre-signed URLs; only lightweight metadata (file id, uploader, timestamp, content-type) is written synchronously to the case’s timeline table.

7.3 Phase 3: Automated Review

Once evidence collection closes (or both parties confirm “no more evidence”), a case-ready-for-review event triggers the Resolution Engine. This is an asynchronous, queue-driven phase — the buyer isn’t waiting on a live HTTP connection, they’re polling or getting a push notification.

7.4 Phase 4: Resolution

Either the automated engine issues a decision, or the case is escalated to a human agent. A resolution event triggers the Ledger Service (if money moves) and the Notification Service.

7.5 Phase 5: Appeal or Close

If neither party appeals within the appeal window, the case auto-closes. If appealed, it re-enters the queue with a flag that forces human review (automated systems don’t get a second try on the same case).

📌
Production example

Amazon’s A-to-Z Guarantee and eBay’s Resolution Center both follow this exact phased shape: a defined evidence window, an automated first pass for low-risk/low-value claims, and a human-reviewed path for anything above a risk or value threshold.

08

Evidence Submission Subsystem

Evidence handling deserves its own deep dive because it’s the part of the system most exposed to large binary payloads, abuse (malware uploads, illegal content), and storage cost at scale.

8.1 Direct-to-storage upload flow

sequenceDiagram participant C as Client App participant EV as Evidence Service participant S3 as Object Storage S3 participant SCAN as Malware Scan Worker participant DB as Case Timeline DB C->>EV: POST /v1/cases/id/evidence-url with file metadata EV->>EV: Validate file type and size limit EV->>S3: Request pre-signed PUT URL S3–>>EV: Return signed URL, expires in 5 minutes EV–>>C: Return signed URL C->>S3: PUT file bytes directly S3–>>C: 200 OK upload complete S3–>>EV: S3 event notification, object created EV->>SCAN: Enqueue scan job SCAN->>S3: Download and scan object SCAN->>DB: Mark evidence as CLEAN or QUARANTINED SCAN–>>EV: Publish evidence-verified event
Figure 8.1 — Evidence upload flow using pre-signed URLs and asynchronous content scanning

Why this design matters at scale: if every evidence photo or video passed through our application servers, a single flash-sale-triggered dispute spike could mean gigabytes per second flowing through stateless compute we’d have to massively over-provision. By letting the client upload directly to object storage, our servers only ever handle small JSON requests for signed URLs — the heavy lifting is delegated to a storage system built for exactly that.

8.2 Content safety scanning

Every uploaded file is asynchronously scanned for malware and, where applicable, run through an image/video moderation model (to catch things like uploaded evidence containing unrelated illegal content). Evidence is not visible to the other party or an agent until it passes scanning — its state sits as PENDING_SCAN until then.

8.3 Immutability & chain of custody

Once evidence is submitted, it is never overwritten. If a user wants to “replace” a photo, the system creates a new evidence record and marks the old one SUPERSEDED — the old file remains retrievable. This matters because appeals and legal escalations sometimes need to see exactly what was originally submitted.

8.4 Sample Java: generating a pre-signed upload URL

EvidenceUploadService.java — issue a short-lived, per-object presigned upload URL
@Service
public class EvidenceUploadService {

    private final S3Presigner presigner;
    private static final long MAX_FILE_SIZE_BYTES = 25L * 1024 * 1024; // 25 MB
    private static final Set<String> ALLOWED_TYPES =
        Set.of("image/jpeg", "image/png", "video/mp4", "application/pdf");

    public EvidenceUploadService(S3Presigner presigner) {
        this.presigner = presigner;
    }

    public PresignedUploadResponse createUploadUrl(String caseId, String userId,
                                                     String contentType, long sizeBytes) {
        if (!ALLOWED_TYPES.contains(contentType)) {
            throw new UnsupportedEvidenceTypeException(contentType);
        }
        if (sizeBytes > MAX_FILE_SIZE_BYTES) {
            throw new EvidenceTooLargeException(sizeBytes);
        }

        String objectKey = "evidence/%s/%s/%s".formatted(
            caseId, userId, UUID.randomUUID());

        PutObjectRequest putRequest = PutObjectRequest.builder()
            .bucket("marketplace-dispute-evidence")
            .key(objectKey)
            .contentType(contentType)
            .metadata(Map.of("case-id", caseId, "uploader-id", userId))
            .build();

        PresignedPutObjectRequest presigned = presigner.presignPutObject(
            b -> b.signatureDuration(Duration.ofMinutes(5)).putObjectRequest(putRequest));

        return new PresignedUploadResponse(presigned.url().toString(), objectKey,
            Instant.now().plus(Duration.ofMinutes(5)));
    }
}
💬
What an interviewer may ask
  • How would you cap total evidence storage cost per case while still letting users upload video?
  • What happens if a client’s direct upload to S3 fails halfway, how do you avoid orphaned partial objects?
  • How do you prevent one party from seeing pending evidence from the other party before it is scanned?
09

Automated Resolution Engine

This is the component that gives the system its scale advantage: the more cases it can safely resolve without a human, the cheaper and faster the whole system is. But automating a decision that involves someone’s money is risky if done carelessly, so this engine is deliberately layered.

9.1 Two-layer decision model

Layer 1

Deterministic Rules Engine

Explainable if-then rules over structured facts: order value, reason code, tracking status, account history. Every rule that fires is logged with its exact condition, so a decision can always be explained in plain language to a regulator, an appeals team, or the user themselves.

Layer 2

ML Risk Scoring

A model outputs a fraud/abuse probability score from 0 to 1, used as a gate rather than the sole decision-maker: only let the rules engine auto-resolve if the risk score is below a threshold. This keeps the “why” auditable while still letting a probabilistic signal veto suspicious cases.

flowchart TD START[“Case Ready for Review
Event from Kafka Queue”] –> FACTS[“Fact Builder Service
Aggregates Order, Tracking,
Account History”] FACTS –> RISK[“ML Risk Scoring Service
Model Server, Outputs 0 to 1″] RISK –> GATE{“Risk Score
Below Threshold?”} GATE — “No, high risk” –> AGENTQ[“Route to Human Agent Queue
Elasticsearch Backed Queue”] GATE — “Yes, low risk” –> RULES[“Rules Engine
Drools or Custom DSL”] RULES –> MATCH{“Matching Rule
Found?”} MATCH — “No” –> AGENTQ MATCH — “Yes” –> DECIDE[“Apply Auto Resolution
Refund, Replace, or Deny”] DECIDE –> LEDGER[“Ledger Service
Execute Refund Transaction”] DECIDE –> NOTIFY[“Notification Service
Inform Both Parties”] DECIDE –> AUDIT[“Audit Log Service
Immutable Decision Record”]
Figure 9.1 — Two-layer automated resolution pipeline: risk-score gate followed by rules engine

9.2 Example rule

Auto-refund rule for low-value not-received cases — deliberately narrow to keep false positives near zero
RULE "auto_refund_low_value_not_received"
WHEN
    reasonCode == "ITEM_NOT_RECEIVED"
    AND orderValue < 25.00
    AND trackingStatus != "DELIVERED_WITH_PHOTO_PROOF"
    AND buyer.disputesLast90Days <= 1
    AND riskScore < 0.20
THEN
    resolution = FULL_REFUND_TO_BUYER;
    holdSellerPayout = false;
    explanation = "Low value, low risk, no delivery photo proof on file";
END
Beginner example

Think of the rules engine as a very strict checklist. If every box on the checklist is ticked — cheap item, no proof of delivery, buyer isn’t a repeat claimer, risk score low — the system says “refund, no human needed.” If even one box is unticked, it goes to a person.

📌
Production example

Airbnb’s resolution flow for low-dollar-amount claims, such as a broken glass reported by a host, is largely automated using similar threshold-based rules, reserving human Trust and Safety agents for high-value property damage or safety-related disputes.

9.3 Sample Java: rules gate combined with risk score

ResolutionDecisionService.java — risk-first gate, then rules match, otherwise escalate
@Service
public class ResolutionDecisionService {

    private final RiskScoringClient riskClient;
    private final RulesEngine rulesEngine;
    private final LedgerClient ledgerClient;
    private final AuditLogger auditLogger;

    public ResolutionDecision decide(CaseFacts facts) {
        double riskScore = riskClient.score(facts);

        if (riskScore >= 0.20) {
            auditLogger.log(facts.caseId(), "ESCALATED_HIGH_RISK", riskScore);
            return ResolutionDecision.escalate(facts.caseId(), "risk_score_too_high");
        }

        Optional<RuleMatch> match = rulesEngine.evaluate(facts);
        if (match.isEmpty()) {
            auditLogger.log(facts.caseId(), "ESCALATED_NO_RULE_MATCH", riskScore);
            return ResolutionDecision.escalate(facts.caseId(), "no_matching_rule");
        }

        RuleMatch rule = match.get();
        ledgerClient.executeResolution(facts.caseId(), rule.outcome());
        auditLogger.log(facts.caseId(), "AUTO_RESOLVED", rule.explanation());

        return ResolutionDecision.autoResolved(facts.caseId(), rule.outcome(), rule.explanation());
    }
}

9.4 Why explainability matters

Every auto-resolution stores the exact rule name and the fact values that triggered it. This isn’t optional polish — regulators, appeals teams, and even the buyer or seller themselves can ask why they were refunded or denied, and the system must answer with a specific reason, not “the algorithm decided.”

💬
What an interviewer may ask
  • How would you safely roll out a new auto-resolution rule without risking a wave of bad refunds?
  • How do you prevent the ML risk model from drifting and silently approving more fraud over time?
  • If two rules both match a case with conflicting outcomes, how do you resolve the conflict?
10

Algorithms, Data Structures & Distributed Systems Foundations

Before going deeper into storage and scaling, it’s worth pausing on the underlying computer-science building blocks this system leans on. These aren’t academic detours — each one directly explains a design decision made earlier in this document.

10.1 CAP Theorem applied to this system

The CAP theorem says a distributed system can only fully guarantee two of three properties during a network partition: Consistency, Availability, and Partition tolerance. Since network partitions are a fact of life at scale, the real choice is between consistency and availability when a partition happens. This system deliberately makes different choices for different data:

DataChoiceReasoning
Ledger transactions (refunds)Consistency over availability (CP)It is better to briefly reject a refund request than to risk double-refunding or losing money during a partition.
Case status reads (cached)Availability over consistency (AP)Showing a buyer a status that’s a few seconds stale is harmless; refusing to show any status at all is a worse experience.
Search index (Elasticsearch)Availability over consistency (AP)An agent’s search results being a few seconds behind the source of truth is acceptable since the case detail page always re-fetches fresh data before any action is taken.
Analogy

Think of CAP like a bank versus a weather app. A bank (our ledger) would rather momentarily refuse to show your balance than show you a wrong one — money mistakes are expensive. A weather app (our case status cache) would rather show you slightly-stale data than no data at all — being a few minutes behind on cloud cover is harmless.

10.2 Consensus and leader election

Our sharded PostgreSQL clusters use a consensus-based failover mechanism (via tools like Patroni, which itself relies on a consensus store such as etcd, built on the Raft algorithm) to elect a new primary if the current one fails. Raft ensures that even though multiple nodes could theoretically claim to be “the leader” after a failure, only one is ever accepted by a majority of the cluster, preventing a dangerous split-brain scenario where two nodes both think they can accept writes.

📌
Software example

If the primary node for shard 7 crashes, the remaining replicas run a Raft-based election. Whichever replica gets votes from a majority of the cluster becomes the new primary within seconds, and PgBouncer connection pools are updated to route new writes there — all without a human needing to intervene at 3 a.m.

10.3 Replication strategies

Within a region, we use synchronous replication to at least one standby, meaning a write isn’t acknowledged to the application until it’s durably stored on more than one node — this protects against losing a case or a refund record if a single machine dies. Across regions, we use asynchronous replication, trading a small window of potential data loss (typically sub-second) for much lower write latency, since waiting for a round trip to another continent on every write would make the system far too slow for a million-requests-a-minute target.

10.4 Partitioning (sharding) strategy

We use a variant of consistent hashing to map buyer_id to a shard. Plain modulo hashing (hash(id) mod N) has a serious weakness: adding or removing a shard (N changes) reshuffles almost every key’s assigned shard, forcing a massive, disruptive data migration. Consistent hashing arranges shards on a conceptual ring so that adding or removing one shard only remaps the small fraction of keys that fell between the old and new boundary, not the entire dataset.

ConsistentHashRing.java — virtual nodes on a hash ring for shard assignment
public class ConsistentHashRing {

    private final TreeMap<Long, String> ring = new TreeMap<>();
    private final int virtualNodesPerShard = 150;

    public void addShard(String shardId) {
        for (int i = 0; i < virtualNodesPerShard; i++) {
            long hash = hash(shardId + "#" + i);
            ring.put(hash, shardId);
        }
    }

    public String getShardFor(String buyerId) {
        long hash = hash(buyerId);
        Map.Entry<Long, String> entry = ring.ceilingEntry(hash);
        if (entry == null) {
            entry = ring.firstEntry(); // wrap around the ring
        }
        return entry.getValue();
    }

    private long hash(String key) {
        return Hashing.murmur3_128().hashString(key, StandardCharsets.UTF_8).asLong();
    }
}

Using many “virtual nodes” per physical shard (150 in the example above) spreads the hash ring evenly, avoiding the situation where one shard randomly ends up owning a much larger slice of the ring, and therefore a disproportionate amount of traffic, than the others.

10.5 Priority queue for the agent workload

The Human Agent Queue doesn’t process cases strictly first-in-first-out. It uses a priority queue (min-heap) ordered by a composite score of SLA deadline proximity and case risk, so a case that’s about to breach its SLA, or one flagged as high fraud risk, jumps ahead of a routine case that still has hours of buffer left.

AgentPriorityQueue.java — heap-ordered queue with SLA-plus-risk composite priority
public class AgentPriorityQueue {

    record QueuedCase(String caseId, Instant slaDeadline, double riskScore) {}

    private final PriorityQueue<QueuedCase> heap = new PriorityQueue<>(
        Comparator.comparingDouble(this::priorityScore));

    private double priorityScore(QueuedCase c) {
        long secondsToDeadline = Duration.between(Instant.now(), c.slaDeadline()).getSeconds();
        // Lower score = higher priority. Urgent deadlines and high risk both lower the score.
        return secondsToDeadline - (c.riskScore() * 3600);
    }

    public void enqueue(QueuedCase c) {
        heap.offer(c);
    }

    public Optional<QueuedCase> nextForAgent() {
        return Optional.ofNullable(heap.poll());
    }
}

10.6 Concurrency control: optimistic locking

Two actors, such as an agent manually resolving a case at the same moment the automated engine finishes its own review, could race to update the same case row. Rather than locking the row pessimistically (which would hurt throughput at scale), we use optimistic concurrency control: every case row carries a version column, and an update only succeeds if the version matches what was read; otherwise it’s retried or rejected with a conflict, forcing whichever actor lost the race to re-read the latest state before acting again.

Optimistic UPDATE with version check — zero affected rows means someone else won the race
UPDATE cases
SET status = :newStatus, version = version + 1, updated_at = now()
WHERE id = :caseId AND version = :expectedVersion;
-- application checks affected row count; 0 rows means a concurrent update won the race

10.7 Networking considerations

Internal service-to-service calls use HTTP/2 (via gRPC for latency-sensitive internal calls like risk scoring) to benefit from multiplexed connections and lower overhead compared to opening a new connection per request. Client-to-gateway traffic stays on standard HTTPS/REST for broad compatibility with web and mobile clients. Keep-alive connection pools between services avoid the cost of repeated TCP and TLS handshakes under high request rates.

10.8 Failure recovery

Every asynchronous step (evidence scanning, resolution processing, notification sending) is designed to be safely retryable: consumers commit their Kafka offset only after successfully completing their work, so a crash mid-processing simply means the event is redelivered and reprocessed, not lost. Idempotency keys (section 13.3) ensure this redelivery never causes duplicate side effects like double refunds.

💬
What an interviewer may ask
  • Why is optimistic locking usually preferred over pessimistic locking for a high-throughput case update path?
  • Walk through what happens end to end if a Kafka consumer crashes exactly after updating the database but before committing its offset.
  • Why does consistent hashing reduce data movement compared to simple modulo hashing when a shard is added?
11

Advantages, Disadvantages & Trade-offs

No architecture is free. Being honest about what this one gives up in exchange for what it delivers is what turns a design into a defensible one under interviewer pressure or an architecture review.

11.1 Advantages of this architecture

Advantages

  • Elastic scale: Stateless services and queue-based decoupling mean traffic spikes are absorbed without redesign, just more pods and more partitions.
  • Explainability: The rules-first automation approach keeps every automated decision human-readable, which matters enormously for trust and for regulators.
  • Fault isolation: Because the Ledger Service is separate and strongly consistent, failures in less critical services (notifications, search indexing) can never corrupt financial state.
  • Incremental automation: New rules can be added or tuned without touching the ML model, and vice versa, so the two layers evolve independently.

Disadvantages and costs

  • Operational complexity: A dozen-plus microservices, several datastores, and an event-streaming backbone require mature DevOps practices; a small team can genuinely struggle to operate this safely.
  • Eventual consistency surprises: Because search and analytics lag the source of truth by seconds, an agent acting purely off a stale search result could occasionally see slightly outdated case counts — the UI must always re-fetch canonical state before any write action.
  • Latency of the async path: Decoupling resolution from case creation means a buyer doesn’t get an instant “you’ve been refunded” — they get “case created, decision in progress,” which is a UX trade-off some teams initially resist.
  • Cost of evidence storage: Video evidence retained for the legal minimum period across millions of cases is a meaningful, ongoing infrastructure cost that must be actively managed with lifecycle policies.

11.2 Key trade-off summary

DecisionChosen approachWhat we gave up
Case creation consistencySynchronous, strongly consistent writeSlightly higher write latency than a fire-and-forget async write
Resolution processingFully asynchronous via KafkaBuyer doesn’t get an instant final answer on complex cases
Search/agent queueEventually consistent via CDCA few seconds of staleness in search results
Automation aggressivenessConservative thresholds, human fallbackLower automation rate than technically possible, in exchange for lower fraud/error risk
12

Databases, Caching & Storage

Different data has different needs. Choosing one datastore for everything at this scale is one of the fastest ways to guarantee a rewrite in two years, so we deliberately mix stores.

12.1 Why sharded PostgreSQL for case data

Case and evidence-metadata records are relational, transactional, and need strong consistency for state transitions, so we use PostgreSQL rather than a pure NoSQL store. But a single PostgreSQL instance cannot handle a million-requests-a-minute write and read load, so we shard by marketplace_region combined with a hash of buyer_id. Every case query in normal operation already includes the buyer or seller ID, so this shard key keeps almost all queries single-shard.

TableStorageNotes
casesSharded PostgreSQLShard key: hash(buyer_id) mod N
case_timeline_eventsSharded PostgreSQL, append-onlyPartitioned by month for retention management
evidence_metadataSharded PostgreSQLFile bytes live in S3; only metadata here
ledger_transactionsSeparate strongly consistent PostgreSQL clusterNever shared with case DB; financial isolation
rules_configSmall PostgreSQL table, heavily cachedRead-heavy, write-rarely
case_search_indexElasticsearchDenormalized, eventually consistent via CDC
evidence_filesS3 / GCS object storageLifecycle policy: move to cold storage after 180 days

12.2 Caching strategy

Redis sits in front of the case database for the hottest read path: “get case status.” Because status changes are relatively infrequent compared to how often a buyer refreshes their case page, a cache-aside pattern with a short TTL (say, 5 seconds) plus explicit invalidation on write dramatically cuts database load.

Analogy

Caching case status is like a restaurant’s order-ready board instead of asking the kitchen directly every time. The board (cache) is updated the moment an order state changes, and everyone glancing at it gets a fast answer without interrupting the cooks (database).

CaseStatusReader.java — cache-aside with short TTL and explicit invalidate on write
@Service
public class CaseStatusReader {

    private final RedisTemplate<String, CaseStatusView> redis;
    private final CaseRepository caseRepository;
    private static final Duration TTL = Duration.ofSeconds(5);

    public CaseStatusView getStatus(String caseId) {
        String cacheKey = "case:status:" + caseId;
        CaseStatusView cached = redis.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }

        CaseStatusView fresh = caseRepository.findStatusView(caseId)
            .orElseThrow(() -> new CaseNotFoundException(caseId));

        redis.opsForValue().set(cacheKey, fresh, TTL);
        return fresh;
    }

    public void invalidate(String caseId) {
        redis.delete("case:status:" + caseId);
    }
}

12.3 A concrete schema sketch

It helps to see actual table shapes rather than just names. Here is a simplified schema for the two most important tables, with comments explaining the reasoning behind each design choice.

Simplified DDL for cases and evidence_metadata — note the version column and object_key pointer
CREATE TABLE cases (
    id                UUID PRIMARY KEY,
    order_id          UUID NOT NULL,
    buyer_id          UUID NOT NULL,
    seller_id         UUID NOT NULL,
    reason_code       VARCHAR(64) NOT NULL,
    status            VARCHAR(32) NOT NULL DEFAULT 'OPEN',
    order_value_cents BIGINT NOT NULL,
    currency          CHAR(3) NOT NULL,
    risk_score        NUMERIC(4,3),
    resolution_type   VARCHAR(32),
    resolution_reason TEXT,
    version           INTEGER NOT NULL DEFAULT 0,   -- optimistic locking, see 10.6
    evidence_deadline TIMESTAMPTZ NOT NULL,
    appeal_used       BOOLEAN NOT NULL DEFAULT FALSE,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Composite index supports the most common lookup pattern: "my open cases"
CREATE INDEX idx_cases_buyer_status ON cases (buyer_id, status);
CREATE INDEX idx_cases_seller_status ON cases (seller_id, status);

CREATE TABLE evidence_metadata (
    id             UUID PRIMARY KEY,
    case_id        UUID NOT NULL REFERENCES cases(id),
    uploader_id    UUID NOT NULL,
    object_key     TEXT NOT NULL,       -- pointer into S3, not the file itself
    content_type   VARCHAR(64) NOT NULL,
    size_bytes     BIGINT NOT NULL,
    scan_status    VARCHAR(16) NOT NULL DEFAULT 'PENDING_SCAN',
    superseded_by  UUID,                -- immutability: point to the newer record instead of overwriting
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_evidence_case ON evidence_metadata (case_id);

Two details worth calling out: the version column exists purely to support the optimistic locking pattern from section 10.6, and object_key in evidence metadata is a pointer, never the file bytes themselves — the database only ever holds small, fast-to-query rows, while the expensive-to-move bytes stay in object storage. This split is what keeps the relational database small and fast even as total evidence volume grows into petabytes over the system’s lifetime.

12.4 Change Data Capture (CDC)

Rather than dual-writing to PostgreSQL and Elasticsearch (which risks the two falling out of sync), we use Debezium to stream row-level changes from PostgreSQL’s write-ahead log into Kafka, and consumers update Elasticsearch and the analytics warehouse from that stream. This keeps the primary write path simple (write to Postgres, done) while still feeding every downstream system.

💬
What an interviewer may ask
  • Why not just use one global database for everything and add read replicas?
  • What are the trade-offs of sharding by buyer id versus by case id?
  • How would you handle a “hot shard” if one region suddenly gets a huge burst of disputes?
13

APIs & Microservices

Every microservice owns one responsibility and its own datastore, communicating only through APIs or events — never by reaching into another service’s tables directly.

13.1 Core API surface

EndpointMethodPurpose
/v1/casesPOSTOpen a new dispute case
/v1/cases/{id}GETFetch case detail and current status
/v1/cases/{id}/evidence-urlPOSTRequest pre-signed upload URL
/v1/cases/{id}/commentsPOSTAdd a message to the case thread
/v1/cases/{id}/appealPOSTFile a one-time appeal
/v1/agent/casesGETAgent queue search, internal only
/v1/agent/cases/{id}/resolvePOSTManual resolution, internal only

13.2 Microservice boundaries

Each service owns exactly one responsibility and its own datastore, communicating with others only via well-defined APIs or events — never by reaching into another service’s database directly.

  • Case Service — case lifecycle and state machine.
  • Evidence Service — upload URL issuance, evidence metadata.
  • Resolution Engine Service — rules + risk scoring + decision.
  • Ledger Service — refunds, holds, payouts (financially isolated).
  • Notification Service — push, email, SMS fan-out.
  • Agent Queue Service — human review workflow.
📌
Production example

This mirrors how large marketplaces separate “Trust and Safety” tooling from core “Payments” — payments teams enforce extremely strict change control and isolation because a bug there directly costs money, while trust and safety tooling can iterate faster.

13.3 Idempotency

Every write API (especially case creation and evidence upload confirmation) accepts an Idempotency-Key header. At a million requests a minute, client retries (due to timeouts, mobile network drops) are guaranteed to happen; without idempotency keys we’d risk duplicate cases or duplicate refunds.

Idempotent createCase controller — returns the previously-stored response for a repeat key
@PostMapping("/v1/cases")
public ResponseEntity<CaseResponse> createCase(
        @RequestHeader("Idempotency-Key") String idempotencyKey,
        @RequestBody CreateCaseRequest request) {

    Optional<CaseResponse> existing = idempotencyStore.find(idempotencyKey);
    if (existing.isPresent()) {
        return ResponseEntity.status(HttpStatus.OK).body(existing.get());
    }

    CaseResponse created = caseService.createCase(request);
    idempotencyStore.save(idempotencyKey, created, Duration.ofHours(24));
    return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
💬
What an interviewer may ask
  • Why should the Ledger Service never be called synchronously from the Case Service’s request path?
  • How would you version these APIs to avoid breaking older mobile app clients?
14

Performance & Scalability at Million-RPM Scale

A million requests per minute (~16,700 RPS sustained, likely bursting to 40,000+ RPS at peak) is the design constraint that shapes almost every decision above. Let’s make the scaling levers explicit.

14.1 Where the load actually goes

flowchart LR TOTAL[“1,000,000 requests per minute
at Edge / CDN”] –> STATIC[“Static + Cached Reads
~55 percent, served by CDN”] TOTAL –> STATUS[“Case Status Reads
~30 percent, served by Redis Cache”] TOTAL –> WRITES[“Case Create / Evidence / Comments
~12 percent, hits API Gateway and Services”] TOTAL –> AGENT[“Agent Console Traffic
~3 percent, internal network”]
Figure 14.1 — Load absorption breakdown at peak: CDN and cache soak the majority before it hits stateful stores

Notice only a fraction of the million requests ever reaches a stateful write path. This is deliberate: CDN and cache absorb the majority, and only genuinely new writes touch the database tier.

14.2 Horizontal scaling of stateless services

Case Service, Evidence Service, and Resolution Engine are all stateless Java services running as Kubernetes Deployments with Horizontal Pod Autoscaling (HPA) based on CPU and request-queue-depth metrics. Because there’s no session affinity requirement, adding capacity is just adding pods behind the existing load balancer — no rebalancing needed.

14.3 Database scaling: sharding + read replicas

  • Sharding spreads writes across N independent PostgreSQL clusters, each responsible for a slice of buyer IDs.
  • Read replicas per shard absorb read traffic (case detail pages, agent search) without competing with write throughput.
  • Connection pooling (PgBouncer) in front of every shard prevents connection exhaustion when hundreds of service pods each try to open direct connections.

14.4 Queue-based load leveling

Kafka partitions for case-events, evidence-events, and resolution-events are sized so that during a burst, producers (the fast, cheap part of the pipeline) never block on consumers (the Resolution Engine, which does heavier work like ML inference). The queue absorbs the burst; consumers catch up at their own sustainable rate. This is the single most important scale decision in the whole design — it converts a bursty, unpredictable inbound load into a smooth, controllable outbound processing rate.

Analogy

A queue here works like a hospital’s waiting room during a mass event. Patients (requests) can arrive faster than doctors (the resolution engine) can see them, but the waiting room (Kafka) holds them safely in order, so the hospital never turns anyone away or collapses — it just processes as fast as it safely can.

14.5 Rate limiting and backpressure

The API Gateway enforces per-user and per-IP rate limits (token bucket algorithm) so a single abusive client can’t consume a disproportionate share of capacity. At the service level, bulkheads (separate thread pools per downstream dependency) stop a slow dependency, such as the ML scoring service, from starving unrelated request handling.

TokenBucketRateLimiter.java — per-user Redis-Lua atomic decrement
@Component
public class TokenBucketRateLimiter {

    private final RedisScript<Long> script = RedisScript.of(
        "local tokens = tonumber(redis.call('get', KEYS[1]) or ARGV[1]) " +
        "if tokens > 0 then " +
        "  redis.call('set', KEYS[1], tokens - 1, 'EX', ARGV[2]) " +
        "  return 1 " +
        "else " +
        "  return 0 " +
        "end", Long.class);

    private final StringRedisTemplate redis;

    public boolean allowRequest(String userId, int capacity, int windowSeconds) {
        Long result = redis.execute(script,
            List.of("rate:" + userId),
            String.valueOf(capacity), String.valueOf(windowSeconds));
        return result != null && result == 1L;
    }
}

14.6 Caching hot paths aggressively

Beyond case status, we cache: rules configuration (rarely changes, read on every decision), buyer/seller dispute-history aggregates (used by the risk model, refreshed every few minutes rather than computed per-request), and eligibility checks for common order types.

14.7 Capacity planning math

At 16,700 RPS sustained across roughly 8 stateless service pods per region per service, each pod handles about 2,100 RPS at low latency for simple JSON operations — well within what a well-tuned Java service on modern hardware can sustain, especially since most heavy lifting is deferred to async queue consumers. Multiplying pod count during peak (holiday season, flash sales) is a pure autoscaling exercise, not a redesign.

💬
What an interviewer may ask
  • If the Resolution Engine’s Kafka consumer lag starts growing during a burst, what’s your mitigation, and what’s the user-facing impact?
  • How would you load-test this system to validate it can truly sustain a million requests per minute?
  • Where’s the single point of contention most likely to appear first as you scale this design up 10x further?
15

High Availability & Reliability

A dispute resolution system going down during a flash-sale return spike is almost as costly as the platform’s checkout going down — buyers panic, sellers panic, and support queues explode.

15.1 Multi-region active-active

Stateless services (Case, Evidence, Resolution) run active-active across at least two regions. Each region has its own database shards, replicated cross-region asynchronously for disaster recovery, but writes for a given user are always routed to their “home region” to avoid cross-region write conflicts.

15.2 Graceful degradation

FailureDegraded Behavior
ML Risk Scoring Service downFall back to a conservative default: route all cases to human agents rather than block case creation
Redis cache cluster downFall back to direct database reads with tighter per-user rate limits to protect the DB
Object storage regional outageQueue evidence uploads for retry; case remains open, just flagged “evidence pending”
Kafka partition unavailableProducers buffer locally and retry with exponential backoff; case creation still succeeds since DB write happens first

15.3 Circuit breakers and retries

Every cross-service call (for example, Resolution Engine calling the ML Risk Scoring Service) is wrapped in a circuit breaker (Resilience4j). If the risk scoring service starts timing out, the breaker trips, and the Resolution Engine immediately falls back to “escalate to human” rather than piling up blocked threads.

Circuit-broken risk-scoring call — fallback returns maximum risk to force human review
@CircuitBreaker(name = "riskScoringService", fallbackMethod = "fallbackToEscalate")
public double score(CaseFacts facts) {
    return riskScoringClient.getScore(facts);
}

public double fallbackToEscalate(CaseFacts facts, Throwable t) {
    log.warn("Risk scoring unavailable for case {}, escalating to agent", facts.caseId());
    return 1.0; // treated as maximum risk, forces human review
}

15.4 Data durability

Evidence in object storage uses cross-region replication and versioning. Case and ledger databases use synchronous replication within a region (for zero data loss on a single-node failure) and asynchronous replication across regions (for disaster recovery within a defined RPO of a few seconds).

💬
What an interviewer may ask
  • What’s your RPO and RTO for the ledger database specifically, and why might it differ from the case database?
  • How do you avoid a “thundering herd” of retries when a downstream service recovers from an outage?
16

Security

Because this system moves money and stores personal evidence, security is not one section — it’s a set of layered defenses baked into every service.

16.1 Authentication & authorization

All client requests carry a signed OAuth2/JWT token validated at the API Gateway. Authorization is enforced at the service layer too (defense in depth): a buyer can only read or comment on cases where they are a listed party, verified against the case record, not just trusted from the token.

16.2 Evidence security

  • Pre-signed URLs expire in minutes and are scoped to a single object key, so they can’t be reused to upload arbitrary files elsewhere.
  • Object storage buckets are private by default; evidence is served to clients only through short-lived signed GET URLs, never public links.
  • All evidence is encrypted at rest (server-side encryption) and in transit (TLS 1.2+).

16.3 Fraud & abuse prevention

Beyond the ML risk score used in resolution, a separate abuse-detection layer watches for patterns like: the same device fingerprint opening disputes across many unrelated accounts, evidence photos that are reused across multiple unrelated cases (reverse image hashing), and velocity anomalies (a buyer suddenly filing 10 disputes in an hour).

16.4 PII and compliance

Evidence and case comments can contain personal data (addresses, phone numbers in chat logs). The system supports redaction on export, per-region data residency (EU case data stays in EU-hosted shards), and a “right to access / right to be forgotten” workflow that anonymizes closed cases past the legal retention period while preserving aggregate audit statistics.

16.5 Rate limiting against abuse

Beyond fairness rate limiting for scale (section 14.5), tighter limits specifically target abuse vectors: capping evidence uploads per case, capping appeal filings, and capping case creation per account per day to blunt refund-fraud rings.

💬
What an interviewer may ask
  • How do you stop a seller from viewing evidence a buyer submitted before the case is officially opened against them, to prevent tampering with their story?
  • How would you detect a coordinated fraud ring opening disputes across many fake buyer accounts?
17

Monitoring, Logging & Metrics

You can’t run this system safely if you can’t see it clearly. Observability isn’t a nice-to-have here — it’s a prerequisite for automating money movement without a permanent human in the loop.

17.1 The three pillars

Metrics

Prometheus + Grafana

Request rate, error rate, p50/p95/p99 latency per service, Kafka consumer lag, cache hit ratio, auto-resolution rate. These are the numbers on-call engineers wake up to when something moves off baseline.

Logging

ELK / OpenSearch

Structured JSON logs shipped to a centralized store, correlated by request ID across every service a request touches, so a single failed case-creation can be pieced together end-to-end even when it crossed six services.

Tracing

OpenTelemetry + Jaeger

Distributed tracing shows a single case-creation request’s full path across API Gateway, Case Service, Kafka, and downstream consumers, so latency regressions can be pinned to the specific hop responsible.

17.2 Business-level metrics

Beyond infrastructure health, the system tracks product-critical numbers: percentage of cases auto-resolved, average time-to-resolution by reason code, appeal rate (a proxy for resolution quality — a rising appeal rate suggests rules or the ML model need review), and agent queue depth versus SLA.

17.3 Alerting

Alerts are tiered: page an on-call engineer for consumer lag exceeding a hard threshold or error rate spikes; ticket (non-paging) for slower-moving signals like a creeping appeal rate. Every alert links directly to the relevant Grafana dashboard and runbook.

flowchart LR SVC[“Microservices
Case, Evidence, Resolution”] –> METRICS[“Metrics Exporter
Prometheus Client”] SVC –> LOGS[“Structured Logs
JSON via Fluent Bit”] SVC –> TRACES[“Trace Spans
OpenTelemetry SDK”] METRICS –> PROM[“Prometheus
Time Series DB”] LOGS –> OS[“OpenSearch
Log Storage and Search”] TRACES –> JAEGER[“Jaeger
Trace Storage”] PROM –> GRAF[“Grafana
Dashboards and Alerts”] GRAF –> PAGER[“PagerDuty
On Call Alerting”]
Figure 17.1 — Observability pipeline: metrics, logs, and traces to dashboards and paging
💬
What an interviewer may ask
  • What single metric would you put on a wall-mounted dashboard to represent overall system health for this product?
  • How would you detect a silent bug where the rules engine is auto-approving too many refunds, before finance notices the cost?
18

Deployment & Cloud

A well-designed architecture with a bad deployment story still ends up as a fragile production system. These are the deployment habits that pair well with the design above.

18.1 Containerized, orchestrated deployment

Every service ships as a Docker container and runs on Kubernetes, with separate node pools for latency-sensitive request-handling services versus batch-style workers (malware scanning, ML inference) that can tolerate more variable scheduling.

18.2 CI/CD pipeline

Code merges trigger automated tests, a container build, deployment to a staging environment running a shadow copy of production traffic, then a canary release (5% of production traffic) with automated rollback if error rate or latency regresses, before a full rollout.

18.3 Infrastructure as Code

All cloud resources (VPCs, Kubernetes clusters, database instances, IAM roles) are defined in Terraform, version-controlled, and reviewed like application code — this makes disaster recovery (“stand up an entire new region”) a repeatable, tested process rather than manual heroics.

18.4 Multi-cloud vs single-cloud consideration

Given the financial and trust-sensitive nature of this system, many marketplaces choose a single primary cloud provider with multi-region redundancy rather than full multi-cloud, since the operational complexity of multi-cloud rarely pays for itself compared to solid multi-region design — multi-cloud is usually reserved for specific regulatory requirements.

18.5 Cost optimization

At the volume this system targets, infrastructure cost becomes a design input, not an afterthought. A few concrete levers matter most:

  • Object storage lifecycle tiers: Evidence older than 30 days with a closed, unappealed case moves to infrequent-access storage, and past the legal retention window moves to archival cold storage or is deleted, cutting storage cost dramatically since the vast majority of evidence is never looked at again after a case closes.
  • Right-sized autoscaling floors: Rather than keeping peak-capacity pod counts running around the clock, minimum replica counts are set for off-peak hours (for example, overnight in a given region) with autoscaling handling the ramp back up before the next business day’s traffic arrives.
  • Spot / preemptible instances for batch workers: Malware scanning and analytics batch jobs, which can tolerate being interrupted and retried, run on cheaper spot capacity, while latency-sensitive request-handling services stay on stable on-demand or reserved capacity.
  • Selective use of serverless: Bursty, infrequent workloads like generating a monthly compliance export are a good fit for serverless functions billed per invocation, rather than paying for always-on capacity that sits idle most of the time.
  • Cache-first reads: Every cache hit avoided a database read; at this scale, a well-tuned cache hit ratio of 90%+ on case-status reads directly translates into a proportionally smaller, cheaper database fleet.
📌
Production example

Large-scale platforms commonly report that object storage lifecycle policies alone — automatically tiering old, rarely-accessed evidence and logs to cold storage — cut storage costs by more than half compared to leaving everything in standard, immediately-accessible storage indefinitely.

💬
What an interviewer may ask
  • Why might you choose Kubernetes over serverless (for example, AWS Lambda) for the Case Service specifically?
  • Where would a serverless approach actually make sense in this architecture?
19

Testing Strategy at Scale

A system that automatically decides refunds needs a testing strategy that goes well beyond typical unit tests, because the cost of a bug here is measured in real money and real trust, not just a failed request.

19.1 Layers of testing

  • Unit tests on rule evaluation logic, state machine transitions, and idempotency handling, run on every commit.
  • Contract tests between services (for example, Case Service and Resolution Engine agree on the exact shape of a case-created event) so a schema change in one service can’t silently break another at runtime.
  • Shadow testing for new rules or a new ML model version: run the new logic against real production traffic in parallel with the live system, log what it would have decided, and compare against actual outcomes before ever letting it make a real decision.
  • Chaos testing — deliberately killing pods, injecting latency into the risk-scoring call, or dropping a Kafka broker in a staging environment to verify the circuit breakers and fallback paths from section 15.3 actually work under real failure, not just in theory.
  • Load testing — synthetic traffic generation (for example, using k6 or Gatling) ramped up to and beyond the million-requests-a-minute target, specifically watching for the first component to show backpressure, since that tells you where the next scaling investment needs to go.

19.2 Load test scenario example

A realistic load test doesn’t send uniform traffic — it models a flash-sale-triggered dispute spike: a sudden 5x jump in case creation over 10 minutes, sustained high evidence-upload volume, and a simultaneous burst of status-check polling from anxious buyers. The test asserts that p99 case-creation latency stays under 300 ms throughout, Kafka consumer lag returns to baseline within a defined recovery window after the spike ends, and no evidence uploads are lost or duplicated.

💬
What an interviewer may ask
  • How would you safely test a change to the auto-resolution rules against real traffic without risking real customer refunds?
  • What’s the difference between a load test and a chaos test, and why do you need both for this system?
20

Design Patterns & Anti-patterns

Patterns are the shorthand experienced architects use to communicate a whole design decision in two words. Anti-patterns are the shorthand for the mistakes those same architects have already made themselves at least once.

20.1 Patterns used

PatternWhere usedWhy
Event-Driven ArchitectureCase, Evidence, Resolution services via KafkaDecouples producers from consumers, absorbs load spikes
CQRS (Command Query Responsibility Segregation)Writes go to PostgreSQL, reads for search served by ElasticsearchOptimizes read and write paths independently
Saga PatternResolution → Ledger refund → Notification sequenceCoordinates a multi-step business transaction across services without distributed locking
Circuit BreakerEvery cross-service callPrevents cascading failure when a dependency degrades
Strangler pattern (evolution)Migrating legacy dispute logic to the new engine incrementallyAllows safe, gradual cutover rather than a risky big-bang rewrite

20.2 Anti-patterns to avoid

Anti-pattern

Synchronous chain of calls for resolution

If case creation directly and synchronously called the resolution engine, which directly called the ML model, which directly called the ledger, a single slow dependency would make every case creation slow or fail. Always decouple with queues at natural business boundaries.

Anti-pattern

One shared database for all services

Letting the Notification Service read directly from the Case Service’s tables creates hidden coupling — a schema change in Case Service silently breaks Notification Service. Communicate only through APIs or events.

Anti-pattern

Black-box ML decisions with no rules gate

Letting an ML model directly issue refunds without an explainable rules layer around it makes disputes about the dispute system itself impossible to resolve, and is a compliance risk that can attract regulator attention.

Anti-pattern

Treating audit logs as an afterthought

Emitting audit records opportunistically from application code (rather than as a first-class, atomic part of every state transition) leads to gaps that only become visible during a legal or regulatory review, when it’s far too late to backfill them.

21

Best Practices & Common Mistakes

Every point in this section comes from the scars of a real production incident somewhere in the industry — either directly, or as an obvious extrapolation from one.

21.1 Best practices

  • Always design the auto-resolution rate as a tunable dial, starting conservative and expanding automation coverage only as confidence in the data grows.
  • Treat the audit log as a first-class, append-only, immutable data store — never let it be an afterthought bolted onto business logic.
  • Version your API contracts and rules configuration; a rule change should be auditable exactly like a code change.
  • Build a “dry run” mode for new rules — evaluate against real traffic without actually applying resolutions, and compare outcomes before enabling.
  • Design for partial failure everywhere evidence, money, and notifications are involved; assume any single call can fail.

21.2 Common mistakes

  • Underestimating evidence storage growth. Video evidence at scale can dwarf all other storage costs; plan lifecycle policies (cold storage, deletion after retention period) from day one.
  • Ignoring appeal loops. Systems that don’t cap appeals or don’t flag repeat-appeal patterns can be gamed into infinite re-review cycles.
  • Conflating case state with UI state. The backend case state machine should be the single source of truth; a common bug is letting the frontend infer status from disparate fields instead of one canonical status field.
  • No fallback when automation dependencies fail. If the risk scoring service being down blocks case creation entirely instead of gracefully escalating, an outage in a “helper” service takes down the “core” feature.
  • Treating the agent console as an afterthought. Because escalated cases are, by definition, the hardest and most ambiguous ones, giving agents a poorly designed queue with weak search and no context aggregation makes the highest-stakes decisions the ones made with the least support — invest in the agent-facing tooling at least as much as the buyer-facing one.
  • Letting reason codes proliferate without governance. It’s tempting to let every team add a new reason code for their specific edge case, but an unbounded, poorly maintained taxonomy eventually makes both the rules engine and the analytics built on top of it unreliable; reason codes should go through the same review process as a schema change.
  • Forgetting that automation decisions still need a human-readable explanation surfaced to the user. A buyer who receives “case denied” with no further detail is far more likely to escalate emotionally, contact support anyway, or leave negative reviews, even if the underlying decision was technically correct — the explanation is part of the product, not just an internal audit artifact.

Taken together, these mistakes share a common thread: they all treat some part of the system — the agent experience, the taxonomy, the explanation shown to users, the failure path — as secondary to the “real” engineering work of services and databases. In a system whose entire purpose is fairness and trust, there is no secondary part; every one of these details is load-bearing for the product’s actual goal.

22

Real-World Examples

Looking at how the largest marketplaces actually solve this problem is the fastest way to sanity-check your own design. Every one of them converges on the same broad shape.

eBay

Resolution Center

Structured, self-service dispute filing with reason codes, evidence upload, and a defined SLA for seller response before eBay can step in and force a resolution — the archetypal design most later marketplaces borrowed from.

Amazon

A-to-Z Guarantee

Automated eligibility checks against order and shipping data, with instant resolution for low-risk cases and escalation to specialist teams for high-value or repeat cases — a real production example of the two-layer decision model described in section 9.

PayPal

Resolution Center

A phased dispute-then-claim model, where a dispute (informal negotiation between buyer and seller) can escalate into a formal claim (PayPal makes the binding decision) if unresolved within a time window — a pattern directly analogous to our auto-resolution-then-agent-escalation design.

Airbnb

Resolution Center

Combines automated, threshold-based payouts for minor damage claims with human specialist review for anything involving safety or high dollar amounts, closely matching the two-layer decision model in section 9.

22.1 What these platforms have in common

Looking across eBay, Amazon, PayPal, and Airbnb, the same underlying shape keeps appearing, which is a strong signal that this shape isn’t accidental but reflects real constraints every large marketplace eventually runs into.

Common traitWhy it recurs
A structured, self-service filing flow with reason codes rather than free-text-only complaintsStructured data is what makes automation and analytics possible in the first place
A defined evidence or response window before escalationGives the other party fair opportunity to respond, which matters both for fairness and for reducing appeal rates
Automatic handling for low-value or low-risk casesThe overwhelming majority of disputes are simple and repetitive; reserving human time for genuinely ambiguous cases is far more cost-effective
A distinct, more heavily audited path for money movementFinancial errors are expensive and regulator-visible in a way that, say, a delayed notification is not
An appeal or escalation mechanismNo automated or even human-first-pass decision is perfect, and platforms need a release valve to catch mistakes before they become reputational damage

This convergence is a useful sanity check when designing your own system: if your design is missing one of these five traits, it’s worth asking explicitly why — sometimes there’s a good reason specific to your product, but often it’s a gap that will surface painfully once real dispute volume arrives.

23

Glossary

A quick reference for the terminology used throughout this tutorial, useful both for onboarding new engineers to the system and for interview prep.

TermMeaning
CaseThe central record representing one dispute between a buyer and a seller tied to one order
Reason codeA fixed, structured category describing why a dispute was opened, such as item not received or item damaged
EvidenceAny file or structured data submitted to support a party’s claim, stored immutably once submitted
Auto-resolutionA decision made without human involvement by the rules engine and risk-scoring gate working together
EscalationMoving a case from automated review into the human agent queue because it failed to meet auto-resolution criteria
AppealA one-time request by either party to have a resolved case reviewed again, always by a human
LedgerThe isolated, strongly consistent subsystem responsible for actual money movement such as refunds and payouts
Idempotency keyA client-supplied unique identifier that lets a retried request be safely recognized and deduplicated rather than processed twice
CDC (Change Data Capture)A technique for streaming row-level database changes into other systems like search indexes without dual writes
Circuit breakerA pattern that stops calling a failing dependency for a cool-down period, preventing cascading failure
Consistent hashingA hashing scheme that minimizes data movement when shards are added or removed
SLAAn internal promise about how fast a given type of case must be handled or first responded to
RPO / RTORecovery Point Objective and Recovery Time Objective — how much data loss and downtime is acceptable during a disaster recovery event
ChargebackA dispute filed directly with a card network or bank rather than through the marketplace’s own resolution system
24

Rollout Strategy for a New Feature Set

If you were building this system from scratch inside an existing marketplace that currently only has manual, agent-driven dispute handling, a big-bang launch of the entire architecture described above would be unnecessarily risky. A more realistic path looks like this.

24.1 Stage 1: Structured intake, fully manual resolution

Replace free-text complaint forms with structured reason codes and a proper evidence-upload flow, but keep every single case routed to a human agent. This alone improves data quality and sets up everything else, while introducing zero automation risk.

24.2 Stage 2: Shadow automation

Run the rules engine and risk-scoring model in shadow mode: every case still goes to a human agent, but the system logs what it would have decided. Compare shadow decisions against actual agent decisions over weeks of real volume to measure accuracy before trusting the system with real money.

24.3 Stage 3: Limited auto-resolution

Turn on auto-resolution only for the narrowest, lowest-risk slice — for example, only for orders under a small dollar threshold with a specific reason code and a very low risk score — and closely monitor the appeal rate on that slice specifically.

24.4 Stage 4: Expand coverage gradually

Widen the rules’ scope and raise the risk-score threshold incrementally, each time observing appeal rate, agent queue relief, and any fraud signal before the next expansion, rather than jumping straight to broad automation.

24.5 Stage 5: Scale-out infrastructure

Only once the product behavior is proven and trusted does it make sense to invest in the full million-requests-a-minute infrastructure — sharding, multi-region deployment, and the complete event-driven pipeline — sized to the growth trajectory the business is actually seeing rather than a hypothetical peak.

Analogy

This staged rollout is like teaching a new employee to approve expense reports. First they shadow an experienced colleague and just watch. Then they get to approve only the smallest, most obviously fine reports on their own. Only after building a track record do they get to handle larger and more ambiguous cases independently — you would never hand a brand-new hire unlimited approval authority on day one, and you shouldn’t hand it to an algorithm either.

25

FAQ, Summary & Key Takeaways

The questions that come up over and over in interviews, architecture reviews, and Slack threads five minutes before a launch. Each answer traces back to a decision made earlier in this tutorial.

Q1

Why not resolve every dispute automatically to save cost?

Because full automation without human oversight on complex or high-value cases risks systematic unfairness that erodes trust faster than the cost savings are worth, and creates compliance exposure when decisions can’t be adequately explained or appealed. The right target is not 100% automation but the largest fraction that remains defensibly fair.

Q2

Why is evidence uploaded directly to object storage instead of through the application servers?

To avoid application servers becoming a bottleneck for large binary payloads at scale — this keeps compute costs proportional to request count, not to file size, and lets storage-purpose-built infrastructure handle the heavy bytes.

Q3

How does this design specifically survive a million-requests-per-minute scenario?

Through layered absorption: CDN and cache handle the majority of reads, stateless services scale horizontally behind load balancers, and Kafka queues convert bursty write/processing demand into a smooth, catch-up-able consumption rate, so no single component ever has to instantaneously handle the full peak load.

Q4

What happens if the automated engine makes a wrong call?

Every decision is explainable and logged, and each party gets exactly one appeal, which forces mandatory human review — this is the safety valve that makes automation acceptable at scale.

Q5

Why separate the Ledger Service from the Case Service instead of keeping refund logic close to case logic?

Money movement has fundamentally different consistency, auditability, and change-management requirements than case workflow logic. Isolating it means the team can apply much stricter review and testing discipline to the Ledger Service alone, without slowing down iteration on less risky parts of the system like notifications or search.

Q6

How does the system prevent a buyer and seller from colluding to abuse refunds?

Abuse-detection signals such as shared device fingerprints, shared shipping addresses across supposedly unrelated accounts, and unusual refund-then-repurchase patterns feed into the risk score, and sustained patterns across many cases are surfaced to a dedicated fraud team through the analytics warehouse rather than being caught case-by-case.

Q7

Could this design be simplified for a smaller marketplace that doesn’t need million-requests-a-minute scale?

Yes — the core ideas (explainable automation, an isolated ledger, immutable evidence, an audit trail) hold at any scale, but a smaller marketplace could reasonably start with a single well-indexed relational database, a simpler synchronous flow, and add sharding, Kafka, and multi-region deployment only once real traffic data justifies the added complexity.

25.1 Key takeaways

📌
The core ideas to carry forward
  • A dispute resolution system is fundamentally an event-driven pipeline: create → collect evidence → decide → act on money → notify → allow appeal.
  • Automation should be a gated, explainable, two-layer decision (deterministic rules + probabilistic risk scoring), never a black box directly moving money.
  • Scaling to a million requests a minute is achieved by pushing as much traffic as possible to CDN and cache, making services stateless and horizontally scalable, and using queues to decouple bursty ingestion from steady processing.
  • Evidence handling is a distinct sub-problem requiring direct-to-storage uploads, async scanning, and immutability guarantees.
  • Financial operations (the Ledger Service) must be isolated, strongly consistent, and never on the same failure domain as the rest of the system.
  • Observability (metrics, logs, traces) and audit trails aren’t optional extras — for a system that decides who gets refunded, they are core requirements.

What makes this system such a rewarding one to study, and to build, is that every architectural choice traces directly to a real, everyday human moment: a package that didn’t arrive, a listing that wasn’t what it looked like, a payment that never landed. The stateless services, the Kafka topics, the sharded databases, the risk-scoring gates — none of them exist for their own sake; they exist so that when Priya opens her case on a Tuesday afternoon, her refund arrives quickly, fairly, and with a clear explanation, and so that her seller, on the other side of the same case, is treated with the same fairness. Design reviews and interviews for this kind of system reward candidates who can hold both perspectives in mind simultaneously — the operational scale of a global platform, and the individual experience of a single frustrated user — and can show how each concrete architectural decision above serves both at once.

25.2 Where to go from here

If you are preparing this design for an interview, practice narrating it from the buyer’s POST all the way to the refund landing in their account, pausing at each hop to justify the specific technology choice with the specific requirement it serves — interviewers reward the candidate who can defend each decision with a concrete constraint rather than the one who lists the most fashionable technologies. If you are building this system for real, resist the temptation to skip straight to the fully-sharded, multi-region version on day one; a correct, observable single-region system with structured reason codes, an isolated ledger, and immutable evidence will teach you more about your actual dispute mix than any amount of premature scaling architecture ever could, and you will earn the right to add each subsequent layer precisely when real production data proves you need it.