Designing Marketplace Seller Onboarding at Scale
A production-grade architecture for onboarding thousands of new sellers a day — verifying who they are, collecting the right tax documents, and wiring up a payout account — while the surrounding platform’s edge tier is built to absorb traffic spikes measured in millions of requests per minute.
Introduction and History
Every two-sided marketplace eventually hits the same wall: buyers are easy to onboard (an email and a password), but sellers are not. The moment a platform lets a stranger receive money — Amazon Marketplace, Airbnb, Uber, Etsy, Upwork, the App Store — it inherits a set of legal and financial obligations that have nothing to do with the product itself and everything to do with banking regulation, tax law, and fraud prevention.
In the earliest days of e-commerce marketplaces, seller onboarding was manual: a business development person emailed a spreadsheet, someone in finance manually wired a bank account into an accounting system, and a compliance officer eyeballed a scanned passport. This worked when a marketplace onboarded ten sellers a month. It collapses completely at the scale modern platforms operate at — Amazon onboards new third-party sellers continuously worldwide, Airbnb onboards new hosts every minute, and Uber/Lyft onboard new drivers in bulk during city launches. At that volume, onboarding has to be a software system, not a process run by humans reading documents.
The turning point in the industry was the emergence of embedded finance platforms — Stripe Connect (2016), PayPal for Marketplaces, Adyen for Platforms — which productized the three hardest pieces of this problem (identity verification, tax form collection, payout rail connectivity) as APIs that any marketplace could integrate rather than build from scratch. This tutorial designs the system that sits around those integrations: the orchestration layer, the data model, and the scale-out architecture a marketplace needs regardless of which underlying KYC or payments vendor it chooses.
Onboarding a seller is like admitting a new vendor to a farmers’ market. The market organizer needs to check the vendor’s ID (identity verification), confirm they have the right tax paperwork to legally sell food (tax documentation), and set up how the vendor actually gets paid at the end of the day (payout setup) — all before the vendor is allowed to set up a single stall. Doing this for one vendor is a five-minute conversation. Doing it for ten thousand vendors applying simultaneously on launch day of a new city requires an assembly line, not a conversation.
1.1 Why marketplaces cannot skip these checks
It is also worth understanding why marketplaces cannot simply skip or defer these checks the way a typical consumer sign-up flow might defer “verify your email” for a few days. Financial regulation in most jurisdictions makes the platform itself liable if it facilitates payments to an unverified or sanctioned party — this is not a product nicety, it is a legal precondition for the seller ever receiving a single dollar. That legal weight is precisely why onboarding architecture deserves the same rigor as a payments or fraud system, even though on the surface it looks like “just another sign-up form.”
1.2 A short history of onboarding at scale
Spreadsheets and scanned passports
Manual onboarding by BD, finance, and compliance staff. Feasible at ten sellers a month, impossible at ten thousand a day.
In-house KYC and payout stacks
Large marketplaces (Amazon, eBay) built bespoke identity verification, tax collection, and payout systems internally — expensive but the only option.
Embedded finance APIs
Stripe Connect, PayPal for Marketplaces, Adyen for Platforms, and Persona/Onfido/Jumio productize KYC/KYB and payout rails as APIs any marketplace can integrate.
Global regulation, per-country rules
Growing tax and data-residency obligations mean modern onboarding has to be country-aware, per-jurisdiction, and continuously updated as rules change.
1.3 Conway’s Law and the three-leg split
This also explains why the three legs of onboarding are so often owned by three different internal teams at a real company: Identity Verification typically sits with a Trust & Safety or Compliance organization, Tax Documentation sits with Finance/Tax, and Payout Setup sits with the Payments team. A system design that forces these three teams to ship changes through one shared, monolithic service creates an organizational bottleneck that mirrors Conway’s Law — the software structure ends up fighting the team structure instead of reflecting it. Designing three independently deployable services from the start avoids that friction as the company scales.
Problem and Motivation
Seller onboarding sits at the intersection of three very different domains, each with its own failure modes and its own external dependencies:
| Domain | Core question | External dependency |
|---|---|---|
| Identity Verification (KYC/KYB) | Is this a real person/business, and are they who they claim to be? | Third-party ID verification vendor (Persona, Onfido, Jumio) |
| Tax Documentation | Do we have the legally required tax form for this seller’s jurisdiction? | Tax form validation service, government TIN-matching APIs |
| Payout Account Setup | Can we legally and technically send this seller money? | Banking rails, card networks, payment processor (Stripe, Adyen, banking partner) |
Each of these steps can independently succeed, fail, or get stuck in a “pending manual review” state — and a seller cannot start selling until all three are complete. This creates a distributed, long-running, multi-step workflow that can take anywhere from seconds (an instantly-verified individual seller) to days (a business seller whose incorporation documents need manual review). The system has to track partial progress accurately, resume where it left off, and never silently drop a seller mid-onboarding.
2.1 The scale problem, framed correctly
Layered on top of this workflow complexity is a genuine scale problem. A marketplace running a major promotional push, a city launch, or a seasonal recruiting campaign (e.g., “become a seller for the holiday season”) can see onboarding-related traffic spike enormously — form submissions, document re-uploads, status polling from mobile apps, webhook callbacks from verification vendors — all hitting the edge simultaneously. The requirement to design for millions of requests per minute at the edge is not about millions of new sellers signing up at once (that would be unrealistic); it is about the read-heavy, poll-heavy, webhook-heavy traffic pattern that surrounds a large in-flight onboarding cohort, plus normal platform traffic sharing the same edge infrastructure. Recognizing this distinction early — between the volume of genuinely new work entering the system and the volume of traffic the system’s public surface must absorb — is the single most important framing decision in this entire design, and it recurs explicitly in the Performance & Scalability section later in this tutorial.
“Why can’t identity verification, tax collection, and payout setup just be three independent sign-up forms a seller fills out in sequence?” — They can be presented sequentially in the UI, but the backend needs to treat them as independently-owned services with their own data stores and retry logic, because each has a different external vendor, a different failure/retry profile, and a different regulatory owner (compliance vs. finance vs. payments) inside the company. Coupling them into one monolithic “signup service” makes it impossible to evolve or scale them independently.
2.2 The product dimension: friction leaks sellers
There is a product dimension to this problem too, often underestimated by engineers focused purely on the backend: onboarding funnels leak sellers at every step, and every additional minute of friction increases drop-off. A marketplace that makes a seller wait on a blocking synchronous call for identity verification loses far more applicants than one that lets the seller keep moving (attaching a payout account, browsing seller tools) while identity review happens in the background. This is a strong product argument for the same asynchronous, decoupled architecture that the scale and reliability requirements independently demand — the business incentive and the engineering incentive point the same direction.
2.3 Iteration speed by domain boundary
Finally, the three-domain split has a subtler benefit for iteration speed. A marketplace expanding into a new country typically needs new tax-form logic and possibly a new payout rail, but rarely needs to change how identity verification works. If Tax and Payout are independently deployable services, that expansion is a change scoped to two services with no risk to Identity Verification’s existing, working logic — exactly the kind of blast-radius containment that motivates a microservices boundary in the first place.
Requirements
3.1 Functional requirements
- A prospective seller can start an application, save progress, and resume later across sessions and devices.
- The system collects identity documents and biometric/liveness data, submits them to a verification vendor, and records the outcome (approved, rejected, needs manual review).
- The system determines the correct tax form per seller type and jurisdiction (e.g., W-9 for US individuals, W-8BEN for non-US individuals, W-9/W-8BEN-E for businesses), validates the submitted Tax Identification Number, and stores the signed form.
- The system collects and validates payout account details (bank account, card, or digital wallet), verifies ownership (e.g., micro-deposits or instant account verification), and links the account to the seller’s profile.
- A seller becomes “active” (able to list products/services and receive payouts) only when all three sub-processes reach an approved state.
- Compliance and support teams can view a seller’s full onboarding history and manually intervene (approve, reject, request re-submission) on any step.
- The system supports re-verification triggers (e.g., periodic re-KYC required by regulation, or a flag raised by a fraud model post-onboarding).
3.2 Non-functional requirements
- Scale: The public-facing edge (API Gateway and Load Balancer tier, CDN, static asset delivery, status-check endpoints) must sustain traffic bursts on the order of millions of requests per minute — roughly 16,000+ requests/second sustained, with burst capacity several multiples higher — without degrading onboarding form submission latency.
- Throughput: Thousands of new seller applications must be processable per day, with the workflow engine able to advance tens of thousands of in-flight onboarding steps concurrently.
- Consistency: A seller must never be marked “active” (able to receive payouts) unless every required step has genuinely passed — this is a hard correctness requirement, not a best-effort one.
- Latency: Form submission and status-check APIs should respond in well under 300ms p99, even though the underlying verification steps themselves are asynchronous and can take minutes to days.
- Auditability: Every decision (approval, rejection, manual override) must be immutably logged with who/what made the decision, for regulatory audits.
- Data protection: Identity documents, tax IDs, and bank details are highly sensitive PII/PCI-adjacent data requiring encryption at rest, strict access control, and jurisdiction-aware data residency.
“The requirement says millions of requests per minute — where does that load actually come from in an onboarding system, given you are not onboarding millions of sellers a minute?” — It comes from the edge tier being shared infrastructure: status-polling from mobile/web clients checking “is my verification done yet,” webhook callbacks from third-party vendors, retried uploads on flaky mobile networks, and the fact that the API Gateway and Load Balancer front the entire platform, not just onboarding. The design has to treat onboarding’s write path (actual applications) as comparatively low-volume and steady, while its read/poll/webhook path shares the same massively-scaled edge as the rest of the platform.
3.3 Two engineering problems, one system
Taken together, these requirements split cleanly into two very different engineering problems that this tutorial solves separately: a correctness problem (never activate a seller who has not genuinely passed every check, never lose track of a partially-completed application) and a scale problem (survive an edge tier absorbing enormous, bursty, largely read-only traffic). Conflating these two problems — for example, by making the correctness-critical write path try to scale the same way as the read-heavy status endpoint — is the most common architectural mistake in systems like this, and avoiding it shapes almost every design decision in the sections that follow.
High-Level Architecture
The architecture separates a horizontally-scaled, stateless edge tier (built to absorb the million-requests-per-minute traffic pattern) from a set of domain-owned onboarding services, each responsible for one leg of the process, coordinated by a central workflow orchestrator.
“Why does the Status Query Service exist separately from the Onboarding Application Service that handles form submission?” — This is a CQRS-style split: writes (submitting a form, uploading a document) are comparatively low-volume and need strong consistency with the orchestrator, while reads (a mobile app polling “what is my status”) are extremely high-volume, latency-sensitive, and can tolerate a few seconds of staleness. Splitting them lets the read path scale independently and be served from cache/read-replicas without ever risking the write path’s correctness guarantees.
Component Deep Dive
5.1 API Gateway Cluster
The single authenticated entry point for every client request. In this system it does more than routing: it distinguishes cheap, cacheable status-check calls from expensive, stateful form-submission calls, and applies different rate-limit tiers to each so that a burst of status polling never starves actual application submissions of capacity.
5.2 Load Balancer
Distributes traffic across a large, autoscaled pool of stateless backend instances. At million-requests-per-minute scale, the load balancer tier itself is deployed as a fleet (not a single box) behind a Layer 4/anycast entry point, with health checks aggressively removing degraded instances from rotation within seconds.
5.3 WAF (Web Application Firewall)
Sits between the CDN and the API Gateway to filter malicious traffic — credential-stuffing attempts against the seller login, scripted bulk-application bots trying to farm referral bonuses, and known attack signatures — before that traffic ever reaches application compute, which is essential when the edge is absorbing millions of requests per minute and cannot afford to spend real backend capacity filtering obvious abuse.
5.4 Onboarding Application Service
Owns the multi-step application form: which sections a seller has completed, draft data for in-progress sections, and validation of individual fields (email format, required fields present) before anything is submitted downstream. Stateless itself; all progress state lives in Redis and the Seller Profile Database.
5.5 Status Query Service
A read-optimized service that answers “what is the current state of my onboarding” without touching the orchestrator or any downstream vendor directly. It reads from a denormalized, cache-friendly view that is updated asynchronously via events, which is what allows it to absorb enormous polling volume cheaply.
5.6 Onboarding Orchestrator
The workflow engine at the center of the system. Tracks each seller’s progress through Identity Verification, Tax Documentation, and Payout Setup as a long-running Saga, calling each domain service, handling asynchronous callbacks/webhooks, applying compensating logic when a step fails, and computing the aggregate “can this seller go active” decision only when every required step is genuinely approved.
5.7 Identity Verification Service
Wraps a third-party KYC/KYB vendor (Persona, Onfido, Jumio, or an in-house solution for regulated markets). Submits documents and biometric data, tracks the vendor’s asynchronous review process, and normalizes vendor-specific outcomes into an internal status model.
5.8 Tax Documentation Service
Determines the correct tax form based on seller type and country, validates the Tax Identification Number format and (where available) against a government TIN-matching API, and stores the completed, e-signed form in the encrypted document store.
5.9 Payout Account Service
Integrates with a payments platform (Stripe Connect, Adyen for Platforms, or direct banking rails) to create a connected payout account, collect and verify bank/card details, and confirm the account is enabled to receive funds in the seller’s jurisdiction.
5.10 Compliance Review Service
Surfaces applications that any automated step flagged for manual review (a mismatched name, a rejected document, a sanctions-list hit) into a queue that human compliance analysts work through, with full context and an audit trail of their decision.
5.11 Notification Service
Sends status updates (document received, verification approved, additional information needed, seller now active) across email/SMS/push, subscribing to the same event bus as every other downstream consumer.
5.12 Analytics and Fraud Pipeline
Consumes onboarding events into a stream-processing/warehouse pipeline to compute funnel drop-off rates per step, detect fraud rings (many applications sharing a device fingerprint or bank account), and feed risk scores back into the Orchestrator to trigger extra scrutiny on suspicious applications.
The Onboarding Orchestrator is like an immigration officer’s checklist clipboard at a border crossing with three separate inspection booths — passport control (identity), customs declaration (tax documentation), and currency exchange setup (payout). The officer does not do any of the three checks themselves; they just make sure a traveler has a stamp from all three booths before waving them through, and know exactly which booth to send someone back to if one stamp is missing.
One design detail worth calling out explicitly: none of these components talk to each other directly. The Identity Verification Service has no API dependency on the Tax Documentation Service, and neither has any awareness that a Payout Account Service even exists. The only component that knows about all three is the Orchestrator, and even it interacts with them exclusively through their public APIs and published events — never through shared database tables or internal method calls. This strict boundary is what allows a team to rewrite the entire Payout Account Service, swap payments vendors, or change its internal data model without coordinating a single line of code change with the Identity or Tax teams.
Onboarding Lifecycle and Data Flow
6.1 Onboarding state machine
As with the order-level status in a fulfillment system, the seller’s overall onboarding status is a derived aggregate computed from the three independent sub-statuses. A seller only transitions to Active when all three sub-processes report an approved state; a rejection in any one of them moves the seller into a rejected or needs-more-info state, without requiring the other two subsystems to know anything about each other.
“What happens if Identity is approved and Payout is approved, but Tax Documentation has been sitting in NeedsMoreInfo for two weeks?” — The seller stays in UnderReview indefinitely at the aggregate level; the Orchestrator should trigger automated reminder notifications on a schedule and, past a defined SLA (e.g., 30 days), either auto-expire the application or escalate it to Compliance Review — the state machine should never allow a seller to silently stall forever with no system-driven follow-up.
Identity Verification (KYC/KYB) Subsystem
Identity verification splits into two flows: KYC (Know Your Customer) for individual sellers, and KYB (Know Your Business) for business entities, which requires verifying the business registration, beneficial ownership, and the identity of the individuals who control it. Both flows submit documents to a third-party vendor and receive an asynchronous verdict.
@Service
public class IdentityVerificationService {
private final KycVendorClient vendorClient;
private final DocumentStore documentStore;
private final IdentityVerificationRepository repository;
public void submitForVerification(SellerId sellerId, IdentityPackage pkg) {
// Idempotency guard: don't resubmit if already pending or approved
Optional<IdentityRecord> existing = repository.findLatest(sellerId);
if (existing.isPresent() && existing.get().getStatus().isTerminalOrPending()) {
return;
}
String docHandle = documentStore.storeEncrypted(pkg.getDocuments());
VendorSubmissionResult result = vendorClient.submit(
sellerId.value(), pkg.getSellerType(), docHandle,
pkg.getLivenessCheckToken());
IdentityRecord record = new IdentityRecord(
sellerId, result.getVendorReferenceId(), VerificationStatus.PENDING);
repository.save(record);
}
// Called by the webhook handler when the vendor completes review
@Transactional
public void handleVendorCallback(String vendorReferenceId, VendorVerdict verdict) {
IdentityRecord record = repository.findByVendorReference(vendorReferenceId)
.orElseThrow(() -> new UnknownVendorReferenceException(vendorReferenceId));
VerificationStatus newStatus = mapVerdict(verdict);
record.transitionTo(newStatus, verdict.getReasonCodes());
repository.save(record);
eventPublisher.publish(new IdentityStepUpdatedEvent(record.getSellerId(), newStatus));
}
}7.1 Handling vendor webhooks reliably
KYC vendors deliver verdicts via webhook, often with at-least-once delivery semantics and no strict ordering guarantee. The webhook handler must be idempotent (keyed on the vendor’s reference ID, not assuming it is the first or only callback for that reference) and must verify webhook signatures to prevent forged approval callbacks — a critical control, since a forged webhook could otherwise be used to fraudulently self-approve identity verification.
@PostMapping("/webhooks/kyc-vendor")
public ResponseEntity<Void> handleWebhook(
@RequestBody String rawPayload,
@RequestHeader("X-Vendor-Signature") String signature) {
if (!signatureVerifier.isValid(rawPayload, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
VendorVerdict verdict = payloadParser.parse(rawPayload);
identityVerificationService.handleVendorCallback(verdict.getReferenceId(), verdict);
return ResponseEntity.ok().build(); // ack quickly; processing is idempotent
}Doing heavy processing synchronously inside the webhook handler. Vendors retry webhooks aggressively if they do not get a fast 2xx response, which can create duplicate-processing storms. The handler should validate the signature, persist the raw event, acknowledge immediately, and let an asynchronous consumer do the actual state-transition work.
7.2 KYB: verifying businesses, not just people
Business sellers add a layer: the platform must verify the business registration itself (e.g., against a corporate registry), identify all beneficial owners above a regulatory ownership threshold (commonly 25% in many jurisdictions), and run each beneficial owner through individual KYC. This means one business application can fan out into multiple parallel identity verification sub-workflows, all of which must complete before the business’s identity step as a whole is considered approved.
“How would you model a business application where one of three beneficial owners fails identity verification?” — Model each beneficial owner’s verification as its own sub-record under the business’s KYB workflow, and define the aggregation rule explicitly (e.g., the business identity step is Approved only if 100% of required beneficial owners pass, and Rejected if any one fails a hard check like a sanctions-list match) — this mirrors the same “derived aggregate status from independent sub-statuses” pattern used at the seller level.
This nested aggregation is a useful general lesson: the same “derive the parent status from independent children” pattern that computes a seller’s overall onboarding status from three sub-steps recurses naturally one level deeper inside KYB, computing the identity sub-step’s own status from multiple beneficial-owner records. Recognizing that a system design pattern applies recursively, rather than inventing a new one-off mechanism for the business-ownership case, keeps the codebase and the mental model consistent as new edge cases are discovered.
Tax Documentation Subsystem
Tax documentation must determine the correct form based on seller type and residency, since submitting the wrong form is a compliance failure even if the seller filled it out honestly.
public enum TaxFormType {
W9, // US individual or entity
W8BEN, // Non-US individual
W8BENE, // Non-US entity
LOCAL_VAT_ID // Jurisdictions requiring VAT/GST registration
}
@Service
public class TaxFormResolver {
public TaxFormType resolve(SellerType sellerType, String residencyCountry) {
boolean isUs = "US".equals(residencyCountry);
if (sellerType == SellerType.INDIVIDUAL) {
return isUs ? TaxFormType.W9 : TaxFormType.W8BEN;
} else {
return isUs ? TaxFormType.W9 : TaxFormType.W8BENE;
}
}
}Once the correct form is presented and completed, the Tax Identification Number itself is validated in two layers: a fast structural check (correct number of digits, valid checksum where applicable) that runs synchronously for instant feedback, and an asynchronous check against a government TIN-matching service (where available, such as the IRS TIN Matching Program in the US) that confirms the number and the associated name actually match on record.
@Service
public class TaxIdValidator {
public ValidationResult validateStructure(String tin, String country) {
TinFormatRule rule = formatRules.get(country);
if (!rule.matches(tin)) {
return ValidationResult.invalid("TIN format does not match expected pattern");
}
return ValidationResult.valid();
}
public void submitForNameMatch(SellerId sellerId, String tin, String legalName) {
// Asynchronous: government matching APIs are often slow or batch-based
tinMatchingClient.submitAsync(sellerId.value(), tin, legalName);
}
@Transactional
public void handleMatchResult(SellerId sellerId, TinMatchResult result) {
TaxRecord record = taxRecordRepository.findBySellerId(sellerId);
record.applyTinMatchResult(result);
taxRecordRepository.save(record);
eventPublisher.publish(new TaxStepUpdatedEvent(sellerId, record.getStatus()));
}
}8.1 Signed document retention
Completed tax forms are legally required to be retained for a fixed number of years (commonly several years past the last reportable payment in the US for W-9/1099 purposes). The signed PDF, along with an audit record of the e-signature (timestamp, IP address, signature method), is stored in the encrypted document store with a retention policy attached at write time, rather than relying on a manual process to remember to keep it.
“What happens if a seller’s TIN structurally validates but fails the government name-match check?” — This should not be an automatic hard rejection, since name-match failures are common for benign reasons (a maiden name, a recent legal name change, a minor formatting difference). Route it to the Compliance Review queue for a human to request a corrected form or supporting documentation, rather than silently blocking the seller with no path forward.
Payout Account Setup
Payout setup connects a seller’s bank account, debit card, or digital wallet so the platform can actually send them money. This subsystem almost always integrates with a payments platform rather than connecting directly to banking rails, since building direct ACH/SEPA/card-network connectivity in-house is a massive undertaking most marketplaces reasonably avoid.
@Service
public class PayoutAccountService {
private final PaymentsPartnerClient partnerClient;
private final PayoutAccountRepository repository;
public void createConnectedAccount(SellerId sellerId, SellerProfile profile) {
ConnectedAccountRequest request = ConnectedAccountRequest.builder()
.country(profile.getCountry())
.businessType(profile.getSellerType())
.email(profile.getEmail())
.build();
ConnectedAccountResult result = partnerClient.createAccount(request);
PayoutAccount account = new PayoutAccount(
sellerId, result.getExternalAccountId(), PayoutStatus.PENDING_VERIFICATION);
repository.save(account);
}
public void attachBankAccount(SellerId sellerId, BankAccountDetails details) {
PayoutAccount account = repository.findBySellerId(sellerId);
partnerClient.attachExternalBankAccount(account.getExternalAccountId(), details);
// Verification (micro-deposits or instant verification) proceeds
// asynchronously; status arrives via webhook.
}
@Transactional
public void handleVerificationWebhook(String externalAccountId, PayoutVerificationEvent event) {
PayoutAccount account = repository.findByExternalId(externalAccountId);
account.transitionTo(event.isVerified() ? PayoutStatus.VERIFIED : PayoutStatus.FAILED);
repository.save(account);
eventPublisher.publish(new PayoutStepUpdatedEvent(account.getSellerId(), account.getStatus()));
}
}9.1 Account verification methods
| Method | Speed | Notes |
|---|---|---|
| Instant verification (bank login via Plaid/Yodlee-style provider) | Seconds | Best user experience; not available in every market or for every bank |
| Micro-deposits | 1–2 business days | Universally available fallback; requires the seller to return and confirm deposit amounts |
| Manual document upload (voided check, bank letter) | Days, human-reviewed | Used when automated methods are unavailable or fail |
Because verification speed varies so widely by method and market, the Orchestrator treats Payout Setup as genuinely asynchronous and unbounded in duration — the same design principle applied to Identity and Tax — rather than assuming it will always resolve quickly.
Allowing a seller to list products before their payout account is verified, planning to “block payouts later” once verification completes. This routinely leads to a backlog of unpaid sellers demanding money the platform cannot yet legally or technically send them, and support escalations that are avoidable by simply not activating listing capability until payout verification is genuinely complete.
Workflow Orchestration (Saga)
Unlike a checkout Saga that completes in seconds, the onboarding Saga is long-running — spanning minutes to weeks — and must persist its state durably between steps rather than holding it in memory, since the process itself is fundamentally asynchronous and interrupted by real-world waiting (a vendor review, a seller returning to confirm micro-deposits).
@Service
public class OnboardingOrchestrator {
private final OnboardingStateRepository stateRepository;
private final EventPublisher eventPublisher;
// Invoked whenever any sub-step publishes a StepUpdated event
@KafkaListener(topics = "onboarding-step-updates")
public void onStepUpdated(StepUpdatedEvent event) {
OnboardingState state = stateRepository.findBySellerId(event.getSellerId());
state.applyStepUpdate(event.getStep(), event.getStatus());
stateRepository.save(state);
AggregateStatus aggregate = evaluateAggregate(state);
if (aggregate == AggregateStatus.ACTIVE && state.getAggregateStatus() != AggregateStatus.ACTIVE) {
state.setAggregateStatus(AggregateStatus.ACTIVE);
stateRepository.save(state);
eventPublisher.publish(new SellerActivatedEvent(event.getSellerId()));
} else if (aggregate == AggregateStatus.REJECTED) {
state.setAggregateStatus(AggregateStatus.REJECTED);
stateRepository.save(state);
eventPublisher.publish(new SellerRejectedEvent(event.getSellerId(), state.getRejectionReasons()));
}
}
private AggregateStatus evaluateAggregate(OnboardingState state) {
if (state.anyStepHardRejected()) {
return AggregateStatus.REJECTED;
}
if (state.allStepsApproved()) {
return AggregateStatus.ACTIVE;
}
return AggregateStatus.UNDER_REVIEW;
}
}This is a choreography-leaning orchestration: sub-services still publish their own step-updated events onto the bus, but a central Orchestrator listens to all of them and owns the aggregate-status decision, giving the system one clear place to reason about “is this seller allowed to go live” rather than scattering that logic across three independent services.
“Why persist the Saga state in a database instead of just relying on Kafka consumer offsets and event replay?” — Consumer offsets tell you what has been processed, not the current business state, and replaying the full event history for every status check would be far too slow for a workflow that can span weeks. A dedicated, queryable OnboardingState table gives O(1) status lookups and survives Orchestrator restarts cleanly, while Kafka remains the durable log of how that state was derived.
Database and Schema Design
CREATE TABLE sellers (
seller_id BIGINT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
seller_type VARCHAR(20) NOT NULL, -- INDIVIDUAL or BUSINESS
country CHAR(2) NOT NULL,
aggregate_status VARCHAR(30) NOT NULL, -- derived, e.g. UNDER_REVIEW, ACTIVE, REJECTED
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE identity_verifications (
verification_id BIGINT PRIMARY KEY,
seller_id BIGINT NOT NULL REFERENCES sellers(seller_id),
vendor_reference_id VARCHAR(128) NOT NULL,
status VARCHAR(30) NOT NULL, -- PENDING, APPROVED, REJECTED, NEEDS_INFO
reason_codes TEXT NULL,
reviewed_at TIMESTAMP NULL,
INDEX idx_seller (seller_id),
INDEX idx_vendor_ref (vendor_reference_id)
);
CREATE TABLE tax_records (
tax_record_id BIGINT PRIMARY KEY,
seller_id BIGINT NOT NULL REFERENCES sellers(seller_id),
form_type VARCHAR(20) NOT NULL,
tin_last_four CHAR(4) NOT NULL, -- never store full TIN unencrypted
status VARCHAR(30) NOT NULL,
document_handle VARCHAR(255) NOT NULL, -- pointer to encrypted document store
signed_at TIMESTAMP NULL
);
CREATE TABLE payout_accounts (
payout_account_id BIGINT PRIMARY KEY,
seller_id BIGINT NOT NULL REFERENCES sellers(seller_id),
external_account_id VARCHAR(128) NOT NULL, -- reference into payments partner
status VARCHAR(30) NOT NULL,
verification_method VARCHAR(30) NOT NULL,
verified_at TIMESTAMP NULL
);
CREATE TABLE onboarding_audit_log (
audit_id BIGINT PRIMARY KEY,
seller_id BIGINT NOT NULL,
step VARCHAR(20) NOT NULL,
old_status VARCHAR(30) NULL,
new_status VARCHAR(30) NOT NULL,
actor VARCHAR(64) NOT NULL, -- system, vendor-webhook, or a compliance analyst id
occurred_at TIMESTAMP NOT NULL
);Notice full Tax Identification Numbers are never stored in plaintext in this table — only the last four digits for display purposes, with the full value held in a dedicated, heavily access-controlled secrets/encryption service (or tokenized via the payments partner) and referenced by a handle. The onboarding_audit_log table is append-only and is the system’s answer to “prove to a regulator exactly what happened and when” for any given seller.
“Would each of Identity, Tax, and Payout own their own database?” — Yes, in a real production system each domain service typically owns its own datastore (potentially even in a different cloud account with stricter access controls for the Tax and Identity services, given the sensitivity of the data), exposing data to the Orchestrator only through events and narrow read APIs rather than shared tables — the schema shown here is simplified for illustration.
APIs and Microservices
| Endpoint | Service | Purpose |
|---|---|---|
POST /v1/onboarding/applications | Application Service | Start a new seller application |
PATCH /v1/onboarding/applications/{id} | Application Service | Save progress on a partially completed form |
GET /v1/onboarding/applications/{id}/status | Status Query Service | Poll aggregate and per-step status |
POST /v1/identity/documents | Identity Verification Service | Upload identity documents for review |
POST /v1/tax/forms | Tax Documentation Service | Submit a completed, e-signed tax form |
POST /v1/payout/accounts | Payout Account Service | Create and link a payout account |
POST /webhooks/kyc-vendor | Identity Verification Service | Internal: receive vendor verdicts |
POST /webhooks/payments-partner | Payout Account Service | Internal: receive bank verification results |
{
"sellerId": "slr_7Q2M9",
"aggregateStatus": "UNDER_REVIEW",
"steps": {
"identity": { "status": "APPROVED", "updatedAt": "2026-08-01T14:02:00Z" },
"tax": { "status": "NEEDS_INFO", "reason": "TIN name mismatch" },
"payout": { "status": "PENDING", "verificationMethod": "MICRO_DEPOSIT" }
}
}The GET status endpoint is deliberately the highest-traffic endpoint in the whole system, since mobile clients typically poll it every few seconds while an application is in review. It never calls the Orchestrator or any downstream vendor synchronously — it reads exclusively from the denormalized, cache-backed view maintained by the Status Query Service, which is what makes it survivable at the traffic volumes described in the requirements.
“Would you recommend polling or push notifications for status updates, given the scale requirement?” — Push (webhooks to the client, or a mobile push notification triggered off the SellerActivatedEvent/StepUpdatedEvent) is architecturally cheaper at scale than polling, since it eliminates the constant background request volume entirely. In practice, shipping both is common: push for the primary UX, with polling as a lower-frequency fallback/reconciliation mechanism for clients that missed a push (backgrounded app, notification permission denied).
Databases, Caching and Load Balancing
13.1 Caching strategy for the status endpoint
Because status queries dominate traffic, the Status Query Service caches each seller’s current status document in Redis, keyed by seller ID, with cache invalidation driven by the same StepUpdated/SellerActivated events every other consumer subscribes to — not by TTL expiry. This means the cache is always at most a few hundred milliseconds stale (the time to process an event) rather than stale for an entire TTL window, while still absorbing the overwhelming majority of read traffic without touching the primary database at all.
@Service
public class StatusQueryService {
private final RedisTemplate<String, String> redis;
private final StatusViewRepository fallbackRepository;
public OnboardingStatusView getStatus(SellerId sellerId) {
String cached = redis.opsForValue().get(cacheKey(sellerId));
if (cached != null) {
return deserialize(cached);
}
// Cache miss fallback: read from the durable, denormalized view table
OnboardingStatusView view = fallbackRepository.findBySellerId(sellerId);
redis.opsForValue().set(cacheKey(sellerId), serialize(view), Duration.ofMinutes(10));
return view;
}
@KafkaListener(topics = {"onboarding-step-updates", "seller-activated"})
public void onStatusChanged(Object event) {
SellerId sellerId = extractSellerId(event);
OnboardingStatusView updated = fallbackRepository.findBySellerId(sellerId);
redis.opsForValue().set(cacheKey(sellerId), serialize(updated), Duration.ofMinutes(10));
}
private String cacheKey(SellerId id) { return "onboarding:status:" + id.value(); }
}13.2 Database choices
The core Seller/Identity/Tax/Payout records need strong consistency and are relatively low-write-volume (thousands of applications a day, not millions of writes a second), making a sharded relational database (PostgreSQL, or a distributed SQL system for very large platforms) the right fit. The audit log, by contrast, is append-only and extremely well suited to a write-optimized, horizontally-scalable store — many platforms route audit events into both the relational store (for quick lookups) and a data lake/warehouse (for long-term compliance retention and analytics).
13.3 Load balancing at million-request scale
| Layer | Technique | Purpose at this scale |
|---|---|---|
| DNS / Anycast | GeoDNS routing to nearest regional edge | Distributes the global request volume across regions before it hits any single data center |
| CDN | Edge caching of static assets and cacheable GET responses | Absorbs a large share of read traffic before it reaches the API Gateway at all |
| API Gateway / L7 LB | Autoscaled gateway fleet, least-connections routing | Handles authenticated, dynamic traffic; scales horizontally with demand |
| Service mesh | Sidecar proxies with circuit breaking and retries | Keeps internal service-to-service calls resilient under load without cascading failure |
“If the edge needs to handle millions of requests per minute, where is the actual bottleneck likely to be?” — Almost never the load balancer or gateway tier themselves, which scale horizontally well; the real bottlenecks are typically stateful resources — database connection pools, cache cluster throughput, and any synchronous call to a slow external vendor. The architecture addresses this by keeping the hot read path (status checks) entirely cache-served and event-driven, and by making every external vendor call asynchronous rather than sitting in the request path.
Advantages, Disadvantages and Trade-offs
Splitting Identity, Tax, and Payout into independently owned services coordinated by an Orchestrator buys resilience and independent scalability at the cost of operational complexity — three (or more) services, three vendor integrations, and a distributed workflow that is inherently harder to reason about than a single synchronous signup form.
It is worth being explicit that this trade-off is not free even when it is the right call. A smaller marketplace onboarding a handful of sellers a week may reasonably choose a simpler, more monolithic implementation and revisit the split once volume and organizational complexity justify it — the architecture in this tutorial is aimed squarely at the “thousands of sellers a day, multiple owning teams, global regulatory footprint” scale named in the requirements, and over-engineering toward it prematurely carries its own real cost in development speed and operational overhead.
| Decision | Advantage | Disadvantage |
|---|---|---|
| Separate services per onboarding domain | Independent scaling, ownership, and vendor swaps without touching other domains | More services to deploy, monitor, and keep consistent |
| Central Orchestrator with derived aggregate status | One clear place to answer “can this seller go active” | Orchestrator becomes a critical, must-be-highly-available component |
| Cache-backed, event-driven Status Query Service | Absorbs enormous polling volume cheaply | Status can lag true state by the time it takes an event to propagate (typically sub-second, but not zero) |
| Third-party KYC/payments vendors instead of in-house | Faster to market, offloads deep regulatory/compliance burden | Vendor lock-in risk, and system correctness partly depends on vendor webhook reliability |
| Durable, persisted long-running Saga state | Survives restarts, supports workflows spanning weeks | More storage and query complexity than an in-memory, short-lived Saga |
Performance and Scalability (Million Requests per Minute)
A million requests per minute is roughly 16,700 requests/second sustained, with realistic burst peaks several times that. Designing for this number means treating the edge tier and the domain-service tier as having fundamentally different scaling requirements, and never letting the slower tier become a dependency of the faster one.
15.1 The read-heavy edge
CDN and edge caching
Absorbs the largest share of this traffic before it reaches any application server — static assets, public marketing/application-start pages, and even short-TTL cached copies of non-personalized responses.
Status Query Service
The single highest-traffic dynamic endpoint and deliberately designed to be a thin Redis read in front of a denormalized view — no joins, no calls to the Orchestrator, no calls to any vendor. Horizontal scaling of stateless service instances behind the load balancer, combined with a sharded/clustered Redis backing store, is what allows near-linear scaling with added capacity.
Tiered rate limiting
Authenticated status polling from a legitimate mobile app gets a generous limit at the API Gateway, while unauthenticated or suspicious traffic patterns (the same client hammering the endpoint far faster than any real UI would) are throttled or challenged by the WAF before consuming backend capacity.
15.2 The write-light, latency-tolerant core
- Actual application submissions, document uploads, and vendor webhook callbacks are orders of magnitude lower in volume than status reads — thousands to tens of thousands per day, not per second — so the Orchestrator, Identity, Tax, and Payout services can be provisioned for a much smaller, steadier baseline load with autoscaling headroom for daily/weekly patterns (e.g., a Monday morning surge) rather than for the same peak as the edge tier.
- Document uploads (identity photos, tax forms) go directly from the client to object storage via a pre-signed upload URL, bypassing application servers entirely for the actual file bytes — the backend only ever handles a small metadata record and a storage handle, which keeps large binary traffic off the request path that needs to stay fast.
15.3 Working through a capacity example
Consider a city-launch campaign where 50,000 new sellers apply over a single day, each polling their status roughly every 10 seconds for an average of 20 minutes of active app usage per day during the review period. That is 50,000 × 120 polls = 6,000,000 status requests spread across the day — a modest number compared to the platform’s baseline traffic from all other sellers and buyers, which is exactly the scenario the million-requests-per-minute requirement is really describing: onboarding’s own write volume stays small, while it shares an edge tier engineered for the platform’s total load.
“Would you scale the Onboarding Orchestrator itself to handle a million requests per minute?” — No, and that is the key insight: the Orchestrator only needs to scale to the actual application-submission and step-update volume, which is far lower. Conflating “the edge must survive massive polling/webhook traffic” with “every backend service must be sized for that same number” leads to wildly over-provisioned, expensive infrastructure for services that do not need it.
15.4 Graceful degradation under extreme load
Even a well-architected edge tier benefits from an explicit degradation strategy for the rare moment traffic exceeds even generous provisioning — a coordinated bot attack, a client-side bug causing a polling storm, or a genuine record-breaking traffic event. Rather than letting the whole platform fail uniformly, the Status Query Service should be able to shed load gracefully: serving a slightly staler cached response with an extended TTL under pressure, returning a clear “retry after N seconds” response with a Retry-After header instead of a hard error, and prioritizing authenticated, known-good traffic over anonymous or newly-seen clients when the system must choose who to serve first. This kind of load-shedding logic belongs at the API Gateway and Status Query Service specifically, since they are the components actually absorbing the million-requests-per-minute traffic pattern described in the requirements.
High Availability and Reliability
Multi-AZ, multi-region edge
The API Gateway, Load Balancer, and Status Query Service are deployed across multiple availability zones and, for global platforms, multiple regions, so a single zone or region outage does not take down onboarding status visibility for the whole platform.
Orchestrator durability
Saga state is persisted to a replicated database on every transition; the Orchestrator itself runs as multiple stateless instances consuming from the same Kafka consumer group, so any instance can pick up and continue processing after a failure.
Webhook durability
Incoming vendor webhooks are persisted to durable storage immediately upon receipt (before any processing), so a downstream processing failure never loses the fact that a webhook arrived — it can always be reprocessed from the stored raw payload.
Circuit breakers
Around every external vendor call (KYC vendor, TIN matching API, payments partner) so a slow or degraded vendor does not exhaust connection pools or cascade into unrelated onboarding steps failing.
Dead-letter queues with alerting
Any onboarding step that fails processing repeatedly is routed to a DLQ so a stuck seller application surfaces to an on-call engineer rather than silently stalling forever.
Regional failover
Onboarding data lives in region-appropriate primaries with cross-region replicas; a regional outage triggers a coordinated failover that promotes the replica and reroutes traffic without losing in-flight application state.
Treating a KYC vendor outage as a hard blocker for the entire onboarding pipeline. A well-designed system lets Tax Documentation and Payout Setup continue processing normally while Identity Verification queues and retries, so a vendor incident degrades onboarding speed for one step rather than halting the whole system.
Security
- Encryption everywhere sensitive: Identity documents, tax IDs, and bank details are encrypted at rest (field-level encryption for the most sensitive fields like full TIN) and in transit (TLS everywhere, including internal service-to-service calls).
- Least-privilege access: Only the Identity Verification Service can read raw identity documents; only the Tax Documentation Service (and a narrowly scoped compliance tool) can read full TINs; the Orchestrator only ever sees status enums, never the underlying sensitive payloads.
- Webhook signature verification on every inbound vendor callback, as shown earlier, to prevent forged approval events.
- PII minimization and data residency: Sensitive documents are stored in region-appropriate storage to satisfy data-residency regulations (e.g., EU seller documents stored in EU-region infrastructure), and retained only as long as legally required before secure deletion.
- Fraud and abuse detection: Velocity checks on applications per device/IP, cross-referencing bank account numbers and identity documents across applications to catch the same fraud ring attempting to onboard many fake seller identities, and step-up friction (additional document requests) for applications matching known fraud patterns.
- Audit-everything: Every automated decision and every human override is logged immutably with actor identity, satisfying both internal security review and external regulatory audit requirements.
“How would you prevent someone from using a stolen identity to onboard as a seller and receive payouts to their own bank account?” — Cross-check the name on the verified identity document against the name on the tax form and the name associated with the payout bank account, flagging any mismatch for manual review rather than auto-approving; also monitor for the same bank account or device fingerprint being reused across many seemingly-unrelated seller applications, a strong signal of a fraud ring rather than isolated legitimate sellers.
17.1 Threat modeling the onboarding pipeline specifically
Onboarding is an attractive target precisely because it sits upstream of money movement — an attacker who successfully onboards a fraudulent seller identity gains a channel to receive payouts, launder funds through fake sales, or harvest referral/promotional incentives at scale. A useful threat-modeling exercise is to walk each of the three subsystems and ask “what does an attacker gain by subverting just this one step, without needing the other two?” Subverting Identity alone (e.g., synthetic identity fraud, using a mix of real and fabricated identity attributes) gains little without a working payout account attached, which is exactly why the aggregate-status gate requiring all three steps to independently pass is a meaningful security control, not just a workflow convenience — an attacker who can fool one vendor’s automated check still has to clear two more independent barriers.
Monitoring, Logging and Metrics
| Metric | Why it matters |
|---|---|
| Edge request rate and latency (p50/p99) | Confirms the gateway/LB tier is absorbing the million-request-per-minute load without degradation |
| Status Query cache hit rate | A drop signals cache invalidation problems or a traffic pattern the cache design did not anticipate |
| Per-step approval/rejection rate | Detects a broken integration (e.g., a vendor API contract change causing mass false rejections) |
| Time-to-active (funnel latency) | Core business metric: how long sellers actually wait to start selling |
| Webhook processing lag | Signals a backlog forming between vendor verdict and internal status update |
| Compliance review queue depth | Operational health of the human-in-the-loop review process |
| DLQ depth per onboarding step | Surfaces stuck applications needing intervention |
Every request carries a correlation ID from the original application submission through every downstream service call, webhook, and event, enabling full-journey tracing for a specific seller’s onboarding in tools like Jaeger or Zipkin — essential when a compliance or support inquiry requires reconstructing exactly what happened and when.
“A seller says they submitted all documents three days ago but their status still shows Pending. How do you debug this at scale, with thousands of applications a day?” — Look up the seller by ID, trace the correlation ID across each service’s logs and the event bus, and check specifically whether a vendor webhook was received and processed for their identity verification reference ID — the majority of “stuck” cases trace back to either a webhook that was never delivered (a vendor-side issue) or one that arrived but failed signature verification or processing, both of which should already be visible in DLQ and error-rate dashboards rather than requiring a cold investigation each time.
Deployment and Cloud Architecture
Each service is independently deployable via Kubernetes, with horizontal pod autoscalers tuned to that service’s actual traffic pattern — the Status Query Service and API Gateway scale aggressively and quickly in response to request rate, while the Orchestrator, Identity, Tax, and Payout services scale on a much gentler curve tied to application submission volume.
- CI/CD: Canary deployments for the edge tier and Orchestrator specifically, given their criticality; a bad deploy to the Status Query Service directly degrades the platform’s ability to serve its highest-traffic endpoint.
- Infrastructure as Code: Terraform manages the Kafka cluster, database clusters, Redis clusters, and autoscaling policies, keeping staging and production environments reproducible and making capacity changes auditable.
- Multi-region for the edge, regional for compliance-sensitive services: The CDN, WAF, and API Gateway run active-active across regions globally; the Identity and Tax services, by contrast, may need to be deployed per-region with data residency boundaries respected, since a European seller’s identity documents may be legally required to stay within EU infrastructure.
- Cost optimization: The bursty, read-heavy edge tier is a good fit for aggressive autoscaling and CDN offload to minimize idle compute cost; the steadier domain-service tier can run on more predictably-sized, reserved capacity.
Design Patterns and Anti-patterns
20.1 Patterns used
CQRS
Separating the write path (Application Service, Orchestrator) from the read path (Status Query Service) to let each scale independently.
Saga (long-running, persisted)
Coordinates Identity, Tax, and Payout as independently-failing steps toward one aggregate outcome, with durable state that survives restarts.
Event-driven architecture
Decouples domain services from each other and from downstream consumers like Notifications and Analytics.
Idempotent receiver
All vendor webhook handlers key on vendor reference IDs and tolerate duplicate delivery, given at-least-once semantics from external systems.
Strategy pattern
Tax-form resolution logic is externalized so new jurisdictions and form types can be added without modifying existing resolution code.
20.2 Anti-patterns to avoid
- Synchronous fan-out at signup: Making the initial application-submission request block on all three vendor integrations completing — this directly couples checkout-style latency expectations to processes that can legitimately take days.
- God Orchestrator: Letting the Orchestrator directly implement KYC document validation logic or tax form parsing itself, rather than delegating to the owning domain service — this recreates a monolith with extra network hops.
- Trusting client-reported status: Allowing a mobile client to claim “identity approved” without the backend independently verifying against the Identity Verification Service’s own record — all activation decisions must be server-side and vendor-verified.
- One-size-fits-all rate limiting: Applying the same rate limit to the high-volume Status Query endpoint and the low-volume application-submission endpoint, which either throttles legitimate polling or leaves the write path under-protected.
Best Practices and Common Mistakes
- Do model each onboarding step’s status independently and derive the aggregate — never collapse three independent processes into one flat “onboarding status” enum.
- Do make every webhook handler idempotent and signature-verified from day one; retrofitting this after a production incident is far more painful than building it in from the start.
- Do separate the read-heavy status-check path from the write path architecturally (CQRS), especially given a scale requirement measured in requests per minute rather than applications per day.
- Do not assume a seller’s onboarding will complete quickly — design the data model, notification cadence, and SLA-escalation logic around processes that can legitimately take days to weeks.
- Do not let any single onboarding step’s vendor outage block the other two steps from progressing — decouple them fully, exactly as digital and physical fulfillment are decoupled in an order-fulfillment system.
- Do build the Compliance Review queue as a first-class feature from launch, not an afterthought — automated KYC/tax/payout checks will never achieve 100% straight-through processing, and a platform without a good manual-review tool will bottleneck on ad hoc spreadsheets and Slack threads instead.
- Do instrument time-to-active and per-step drop-off as product metrics from day one, not just technical SLA metrics — a step that is technically fast but confusingly worded can leak just as many sellers as a step that is technically slow.
- Do not hard-code jurisdiction-specific rules (which tax form applies, which identity document types are accepted, which payout rails exist) directly into service logic — externalize them into configuration or a rules engine, since a marketplace expanding into new countries will need to add and adjust these rules constantly without a full redeploy.
- Do give Compliance analysts a single-pane view of a seller’s entire onboarding journey (all three steps, all vendor responses, all prior manual notes) rather than three separate tools per domain — the org chart may be split three ways, but the human reviewing an edge case needs the full picture in one place.
Real-World Industry Examples
Stripe Connect
Productizes exactly the Identity and Payout legs of this architecture as an API — “Connected Accounts” handle KYC/KYB and payout rail setup, letting a marketplace focus its own engineering on the Orchestrator, Application Service, and Compliance Review tooling described in this tutorial rather than building identity verification or banking connectivity from scratch.
Airbnb
Onboards hosts through a multi-step flow that separates identity verification from payout setup, and treats them as independently-trackable steps in the host’s dashboard — a host can complete listing setup before payout verification finishes, but cannot receive a payout until it is done, mirroring the “activate only when all required steps pass” rule in this design.
Uber / Lyft
Driver onboarding at city-launch scale is a well-known industry example of exactly the burst-traffic problem this tutorial addresses — thousands of drivers applying and checking application status simultaneously during a city launch, with document upload (license, insurance, vehicle registration) and background-check verification running as genuinely asynchronous, independently-tracked steps.
Amazon Seller Central
Business sellers go through KYB-style verification including business registration and beneficial-owner checks, tax interview flows that branch by seller country, and bank account verification — all visible to the seller as a checklist of independent steps, consistent with the per-step status model described here.
Etsy
A useful illustration of scale asymmetry — Etsy’s seller base is enormous and largely composed of individual/micro-business sellers, meaning the platform’s onboarding system leans heavily on lightweight, largely automated KYC flows for individuals while reserving deeper KYB scrutiny for the smaller population of larger business sellers, an example of tuning verification depth to seller risk profile rather than applying one uniform process to every applicant.
DoorDash / Instacart
Gig-worker onboarding at scale (Dashers, shoppers) shares the exact burst-traffic and background-check-latency characteristics this tutorial designs for — a promotional recruiting push can drive thousands of applicants in a single day, each polling a status screen while a background check and payout setup complete asynchronously over hours to days.
Across all of these examples, the common architectural thread is the same: the seller/host/driver-facing experience is a single, unified checklist, while the backend genuinely runs three-plus independent pipelines with different vendors, different latencies, and different failure modes — exactly the separation of concerns this tutorial’s Orchestrator and per-step state machine are designed to express cleanly in code and in data.
Frequently Asked Questions
Should a seller be allowed to browse/list products while onboarding is still in progress, or only after full activation?
Most platforms allow draft listing creation during onboarding (good UX, keeps the seller engaged) but gate the listing from actually going live and gate payout capability behind full activation — this is a product decision layered on top of the architecture, and the state machine described here supports either choice by exposing granular per-step status to the frontend.
How do you handle a seller who needs to be re-verified periodically due to regulation?
Model re-verification as a new onboarding-step instance tied to the existing seller record rather than a brand-new application — the same Identity Verification Service and Orchestrator machinery applies, triggered by a scheduled job or a risk-score threshold rather than a fresh seller signup event.
What happens if the same person tries to submit multiple seller applications with different identities?
This is exactly what the fraud/analytics pipeline’s device-fingerprint and cross-application correlation is for — flag applications that share strong signals (same device, same bank account, same document image hash) with a previously rejected or suspicious application, and route them to Compliance Review rather than processing them as unrelated new applicants.
Why not just build one combined “onboarding service” instead of three separate domain services plus an orchestrator?
At low scale, this is a reasonable simplification. At the scale described in this tutorial — thousands of applications daily, an edge absorbing millions of requests per minute, and three domains owned by different internal teams (compliance, tax/finance, payments) with different vendors and different regulatory obligations — a combined service becomes a deployment and ownership bottleneck almost immediately, which is why the split described here is the pattern used by essentially every marketplace operating at this scale.
How would you extend this system to support onboarding for a completely new seller category, such as a rental-property host in a country the platform has never operated in before?
Because tax-form resolution and identity-document requirements are already externalized as configuration/rules rather than hard-coded logic, adding a new country typically means adding new rule entries (which tax form applies, which document types the KYC vendor should request, which payout rails are available) rather than writing new service code — the Orchestrator, event contracts, and state machine stay unchanged, which is the payoff of designing the domain boundaries correctly from the start.
How does this system avoid becoming a bottleneck during a coordinated marketing push that drives a sudden spike in new applications?
The Application Service and Orchestrator are still stateless and horizontally scalable, so a genuine spike in applications (as opposed to status polling) scales the same way any other stateless service would — the difference is simply that this spike is orders of magnitude smaller than the edge tier’s steady-state polling and webhook volume, so it rarely requires the same aggressive autoscaling posture as the Status Query Service and API Gateway.
Summary and Key Takeaways
Key takeaways
- Treat Identity Verification, Tax Documentation, and Payout Setup as independently owned, independently failing services, coordinated by a durable, long-running Saga rather than a synchronous signup flow.
- Derive the seller’s aggregate onboarding status from three independent sub-statuses; never collapse them into one flat field.
- Separate the high-volume, poll-heavy status-read path (CQRS) from the low-volume, correctness-critical write path — this is the key to surviving a million-requests-per-minute edge without over-provisioning the entire backend to match.
- Make every webhook handler idempotent and signature-verified; external vendors deliver verdicts asynchronously and unreliably, and the system’s correctness depends on handling that gracefully.
- Build human-in-the-loop Compliance Review as a first-class part of the system from day one — full automation of KYC, tax, and payout decisions is neither realistic nor, in many jurisdictions, legally sufficient on its own.
- Match provisioning to actual traffic shape rather than a single headline number — the edge tier scales to millions of requests per minute of largely read-only, poll-and-webhook traffic, while the domain services underneath scale to a comparatively modest, steady volume of genuine applications.
The throughline across this design is the same principle that governs almost every large-scale marketplace system: keep the customer-facing (here, seller-facing) experience simple and unified — one application, one status page — while pushing the real complexity into independently scaling, independently failing backend services connected by durable, asynchronous workflows rather than fragile synchronous chains. Get that separation right, and the system can absorb a city launch, a holiday recruiting campaign, or a new country’s regulatory requirements without the underlying architecture needing to change — only the configuration and the scale of each already-independent piece.
For an engineer walking into an interview with this system design, the strongest signal to communicate is not memorized component names but this underlying reasoning: identify what must be strongly consistent (the activation decision), what can be eventually consistent (status visibility), what is inherently slow and external (vendor verification), and what is inherently fast and internal (cache-served reads) — then let those distinctions, not habit or convention, drive every architectural boundary in the diagram.