Dispute Service Low-Level Design: Chargeback Flow, Evidence Collection, and Resolution Workflow

Dispute Service Low-Level Design

The dispute service manages the chargeback lifecycle: intake of issuer-initiated chargebacks, collection of merchant evidence, automated pre-arbitration responses, and final resolution tracking. Chargebacks carry hard card network deadlines (typically 20 days), making workflow automation and alerting critical design concerns.

Requirements

Functional

  • Ingest chargeback notifications from acquiring banks via file feed or API webhook.
  • Route each dispute to the appropriate merchant account and match to the original transaction.
  • Collect evidence documents from merchants (delivery proof, communication logs, policies).
  • Automate pre-arbitration response assembly for common reason codes.
  • Track resolution outcomes (won, lost, expired) and update the financial ledger.

Non-Functional

  • Deadline enforcement: alerts at 50% and 90% of response window; hard block on submission after deadline.
  • No missed chargebacks: at-least-once ingestion with deduplication on acquirer reference number.
  • Evidence documents stored durably for the full card network retention period (18 months minimum).

Data Model

  • dispute: id (UUID), authorization_id, acquirer_id, acquirer_case_ref (UNIQUE per acquirer), reason_code, amount, currency, status (OPEN | EVIDENCE_NEEDED | SUBMITTED | PRE_ARBITRATION | WON | LOST | EXPIRED), response_deadline, created_at, updated_at, version.
  • dispute_evidence: id, dispute_id, evidence_type (DELIVERY_PROOF | COMMUNICATION | POLICY | REFUND_PROOF | OTHER), file_key (S3 key), uploaded_by, uploaded_at, verified.
  • dispute_response: id, dispute_id, response_type (REPRESENTMENT | PRE_ARBITRATION | ACCEPT), assembled_at, submitted_at, submitted_by, outcome.
  • dispute_event: id (sequence), dispute_id, event_type, payload (JSONB), actor, occurred_at — append-only.
  • deadline_alert: id, dispute_id, alert_threshold_pct, fired_at, channel (EMAIL | SLACK | PagerDuty).

Core Algorithms and Flows

Chargeback Intake and Deduplication

Acquirers deliver chargebacks via daily batch files (ISO 8583 or proprietary CSV) and real-time webhooks. A parser normalizes both formats into a canonical dispute record. Before inserting, the system checks for an existing row with the same acquirer_id and acquirer_case_ref combination. Duplicate notifications are discarded and acknowledged; novel chargebacks are inserted with status OPEN and a deadline computed from the acquirer-reported dispute date plus the reason-code-specific response window defined in a configuration table.

Evidence Collection

Merchants upload evidence documents through a pre-signed S3 URL flow: the service issues a pre-signed PUT URL, the client uploads directly to S3, and a completion callback marks the dispute_evidence row as uploaded. A background validator checks file type, size limits, and scans for malware via an async ClamAV sidecar. Verified evidence is associated with the dispute and becomes eligible for inclusion in a response package.

Automated Pre-Arbitration Assembly

For high-frequency reason codes (e.g., “item not received” with proof of delivery on file, or “duplicate charge” where a refund was already issued) the system runs a rule engine at evidence collection completion. Rules are stored as configuration and evaluated in priority order. If a rule matches and all required evidence types are present, the service assembles a response PDF, submits it to the acquirer API, and advances the dispute to SUBMITTED without human intervention. Unmatched disputes remain in EVIDENCE_NEEDED for merchant review.

Resolution State Machine

Allowed transitions: OPEN → EVIDENCE_NEEDED | ACCEPT; EVIDENCE_NEEDED → SUBMITTED | ACCEPT | EXPIRED; SUBMITTED → WON | LOST | PRE_ARBITRATION; PRE_ARBITRATION → WON | LOST | EXPIRED. On WON the chargeback amount is reversed in the ledger. On LOST the amount is written off. On EXPIRED the dispute is closed as LOST and an alert is sent to the compliance team. All transitions append a dispute_event row before committing the status update, providing a full timeline for card network audits.

API Design

  • GET /v1/disputes/{id} — returns dispute status, reason code, deadline, and evidence list.
  • POST /v1/disputes/{id}/evidence — requests a pre-signed upload URL for an evidence document.
  • POST /v1/disputes/{id}/evidence/{evidence_id}/complete — marks upload complete; triggers validation.
  • POST /v1/disputes/{id}/respond — merchant manually triggers response assembly and submission.
  • POST /v1/disputes/{id}/accept — merchant accepts the chargeback, skipping representment.
  • GET /v1/disputes?status={s}&deadline_before={t}&cursor={c} — paginated list for ops dashboard and deadline monitoring.
  • POST /v1/webhooks/acquirer-disputes — acquirer delivers new chargeback or status update notifications.

Scalability Considerations

  • Deadline alert scheduler runs as a cron job querying disputes where response_deadline is within 24 hours and no alert has been fired at the relevant threshold; it is idempotent because fired alerts are recorded in deadline_alert.
  • Evidence validation workers pull from a queue and scale independently; malware scanning is CPU-intensive and isolated from the API tier to avoid latency spikes.
  • Automated rule evaluation is triggered by an event on the EVIDENCE_NEEDED → evidence verified transition, decoupling assembly from the upload request path.
  • Acquirer API submission uses a retry queue with exponential backoff; each retry carries the same idempotency key so acquirers deduplicate on their side.
  • Dispute volume is low relative to transaction volume (typically under 1%) but deadline sensitivity is high; the scheduler uses a priority queue ordered by deadline to ensure the most urgent disputes are processed first during catch-up after an outage.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does chargeback intake flow work in a dispute service?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “When a card network issues a chargeback, the payment gateway sends a webhook with the dispute ID, amount, reason code, and response deadline. The dispute service ingests this event, creates a dispute record linked to the original order, deducts the disputed amount from the merchant's settlement balance, and notifies the merchant team. A response deadline timer is started. The dispute enters the EVIDENCE_REQUIRED state and is assigned to the disputes queue for review.”
}
},
{
“@type”: “Question”,
“name”: “How is evidence collection implemented with pre-signed URLs in a dispute service?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The service generates pre-signed S3 PUT URLs for each evidence document type (delivery confirmation, customer communication, product description). These URLs are returned to the merchant portal with short TTLs (e.g., 15 minutes). The merchant uploads files directly to S3 without routing through the application server. On upload completion, S3 triggers a Lambda or SQS message that registers the document in the dispute record. This pattern avoids large file uploads through the API layer and scales storage independently.”
}
},
{
“@type”: “Question”,
“name”: “What is automated representment in a dispute service?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Automated representment submits the evidence package to the card network via the gateway API without manual intervention for disputes that meet confidence thresholds — e.g., tracking shows delivered, signature obtained, no prior refund issued. The service assembles the evidence bundle, calls the gateway's dispute submit endpoint, and transitions the dispute to REPRESENTED. A rules engine determines which disputes qualify for automation based on reason code, order value, and evidence availability. Low-confidence cases are routed to manual review.”
}
},
{
“@type”: “Question”,
“name”: “What does a resolution state machine look like for disputes?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “States: EVIDENCE_REQUIRED → SUBMITTED → (WON | LOST | ACCEPTED). EVIDENCE_REQUIRED allows evidence upload and moves to SUBMITTED on representment. SUBMITTED is terminal pending the card network's decision webhook. WON triggers reversal of the provisional debit on the merchant balance. LOST finalizes the debit and optionally triggers recovery workflows. ACCEPTED (merchant concedes) skips representment and finalizes the debit immediately. All transitions are time-stamped and immutable for audit purposes.”
}
}
]
}

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: Shopify Interview Guide

Scroll to Top