Customer Onboarding System Design
Design a scalable enterprise Customer Onboarding System — covering multi-channel intake, identity verification, KYC/AML compliance, account provisioning, product enrollment, welcome journey orchestration, progressive profiling, and regulatory audit trail.
1-Hour Interview Roadmap
| Time | Topic |
|---|---|
| 0 – 5 min | Requirements clarification |
| 5 – 10 min | Capacity estimation |
| 10 – 18 min | High-level architecture + core services |
| 18 – 28 min | Onboarding lifecycle — registration through account activation |
| 28 – 36 min | Identity verification + KYC/AML compliance |
| 36 – 43 min | Account provisioning + product enrollment |
| 43 – 50 min | Welcome journey orchestration + progressive profiling |
| 50 – 56 min | Database design + API design |
| 56 – 60 min | Trade-offs + common interview mistakes |
What Are We Building?
An enterprise Customer Onboarding System (COS) that orchestrates the complete journey from a new customer's first interaction to a fully activated, product-enabled account — spanning identity verification, compliance checks, account creation, product provisioning, and the welcome experience.
Customer onboarding is the first real experience a customer has with a business after signing up. A slow, confusing, or broken onboarding process directly causes abandonment — industry data shows 60–75% of customers abandon onboarding if it takes more than 5 minutes or has unclear steps. A well-designed onboarding system is both a compliance requirement and a revenue driver.
Scale reference: Stripe onboards thousands of businesses per day. Robinhood onboarded 3 million customers in Q1 2021 alone. A mid-tier fintech or SaaS company onboards 1,000–50,000 customers per month. Design for 100,000 customers per month (~3,333/day, ~140/hour average) with peak spikes of 10× during campaigns.
Key unique challenges:
- Multi-channel entry — customers arrive from web, mobile, referral links, partner APIs, and agent-assisted flows; each channel has different data availability and UX constraints
- Regulatory compliance — financial services require KYC/AML; healthcare requires HIPAA; regulated industries have mandatory verification steps that cannot be skipped or reordered
- Partial completion and resumption — most customers do not complete onboarding in one session; the system must save progress and allow seamless resumption days later
- Progressive profiling — collecting all required information upfront causes abandonment; the system must request only what is needed now and gather the rest over time
- Downstream provisioning — creating an account triggers provisioning across multiple systems (CRM, billing, product entitlements, communication platforms) that each have their own latency and failure modes
Step 1 — Requirements
Functional Requirements
| # | Requirement |
|---|---|
| 1 | New customer can start registration via web, mobile app, partner API, or agent-assisted portal |
| 2 | System collects customer profile information progressively — only required fields at each step |
| 3 | System verifies customer identity via KYC with government ID document check |
| 4 | System screens customer against AML/sanctions lists (OFAC, PEP lists) |
| 5 | Customer verifies email address and phone number via OTP before account activation |
| 6 | System creates customer account in the core platform and assigns a unique customer ID |
| 7 | System provisions customer in all downstream systems: CRM, billing, product entitlements |
| 8 | System enrolls customer in selected products or plans at signup |
| 9 | System delivers a personalized welcome journey: welcome email, feature tour, setup checklist |
| 10 | Customer can pause and resume onboarding across sessions and devices |
| 11 | System tracks onboarding completion percentage and step-by-step funnel analytics |
| 12 | Operations team can view onboarding status, abandonment points, and intervene for stuck customers |
Non-Functional Requirements
| # | Requirement |
|---|---|
| 1 | Registration form submission responds within 2 seconds (p99) |
| 2 | Account activation (KYC cleared + all systems provisioned) completes within 30 seconds for auto-approved flows |
| 3 | Zero customer loss — no submitted registration can be silently dropped |
| 4 | Onboarding state must be durable — customer can resume after days, weeks, or device changes |
| 5 | Downstream provisioning must be idempotent — network retries cannot create duplicate CRM contacts or billing subscriptions |
| 6 | GDPR / CCPA compliance — customer can delete their account and all associated data at any point |
| 7 | Full audit trail — every step, decision, and verification result permanently logged |
| 8 | Support A/B testing — onboarding flow variant assignment tracked per customer |
| 9 | High availability — 99.9% uptime for registration and account activation APIs |
Out of Scope
- Post-onboarding customer lifecycle management (upgrades, renewals)
- Product-specific configuration (handled by each product's setup wizard)
- Support ticket creation and resolution
- Billing payment method collection (handled by Billing Service)
- Agent/rep compensation for assisted onboarding
Step 2 — Capacity Estimation
Traffic Estimates
Monthly registrations: 100,000
Daily registrations: 100K / 30 = ~3,333/day
Average rate: 3,333 / 86,400 = ~0.04 reg/sec
Peak rate (campaign): 10× = ~0.4 reg/sec = ~24/minute
Onboarding steps per customer (avg): 8 steps
Email verification: 1
Phone verification: 1
Profile completion: 2–3 steps
Identity verification (KYC): 1
Plan selection: 1
Confirmation: 1
Step completion events/day: 3,333 × 8 = ~26,664/day = ~0.3 events/sec
Peak (campaign): ~3 events/sec
Downstream provisioning calls per customer:
CRM contact: 1
Billing subscription: 1
Product entitlement: 1–3
Email platform: 1
Analytics: 1
Total: ~6 provisioning calls per customer
Daily provisioning calls: 3,333 × 6 = ~20,000/day
Storage Estimates
Per customer onboarding record:
Customer profile data: ~2 KB
Onboarding step states: ~500 bytes × 8 steps = 4 KB
KYC result + documents: ~20 KB metadata + docs in S3
Audit events: ~200 bytes × 15 events = 3 KB
Total per customer: ~9 KB (metadata)
Annual metadata storage:
100K/month × 9 KB × 12 = ~10.8 GB/year (trivial)
ID document storage (S3):
100K/month × 3 images × 500 KB = ~150 GB/month
1-year retention: ~1.8 TB
Funnel analytics events:
100K customers × 20 funnel events × 200 bytes = ~400 MB/month
Key Insights
- Volume is moderate, orchestration complexity is high — 3,333/day is low throughput; challenge is multi-system coordination, not raw scale
- Partial completion rate is high — industry averages show 40–60% of registrations never complete; system must handle millions of incomplete records efficiently
- Provisioning is the latency bottleneck — 6 downstream calls during activation; must be parallelized and fault-tolerant
- Peak campaign load — 10× spikes during promotions; queue-based provisioning absorbs bursts without data loss
- Document storage dominates — 1.8 TB/year for ID documents; requires object storage with compliance-grade retention
Step 3 — High-Level Architecture
flowchart TD
Channels["Entry Channels\nWeb / Mobile / Partner API / Agent Portal"]
AG["API Gateway\nAuth + Rate Limiting + TLS"]
ORC["Onboarding Orchestrator\n(State machine + step router)"]
VER["Verification Service\n(Email + Phone OTP)"]
KYC["KYC/AML Service\n(Identity + sanctions)"]
PROF["Profile Service\n(Customer data + progressive collection)"]
PROV["Provisioning Service\n(Downstream system fan-out)"]
WJ["Welcome Journey Service\n(Email sequences + in-app checklists)"]
ANL["Analytics Service\n(Funnel + A/B tracking)"]
AuditS["Audit Service\n(Append-only event log)"]
CustDB[("PostgreSQL\nCustomer DB")]
DocStore[("S3\nID Document Store")]
KF["Kafka\nOnboarding Events"]
REDIS["Redis\nOTP Store + Step Cache + Idempotency"]
Channels --> AG
AG --> ORC
ORC --> VER
ORC --> KYC
ORC --> PROF
ORC --> PROV
ORC --> CustDB
ORC --> REDIS
ORC --> KF
KYC --> DocStore
KF --> WJ
KF --> ANL
KF --> AuditS
PROV --> CRM["CRM\n(Salesforce / HubSpot)"]
PROV --> BIL["Billing\n(Stripe / Zuora)"]
PROV --> ENT["Entitlements\n(Product Access)"]
PROV --> COM["Communication Platform\n(SendGrid / Twilio)"]
Component Responsibilities
| Component | Responsibility |
|---|---|
| Onboarding Orchestrator | Central state machine — drives customer through steps, persists progress, coordinates all services |
| Verification Service | Email OTP delivery and verification; phone SMS OTP; tracks verification attempts and expiry |
| KYC/AML Service | Government ID document verification; liveness check; OFAC and PEP list screening |
| Profile Service | Stores and validates customer profile data; enforces progressive collection schema |
| Provisioning Service | Fan-out to downstream systems (CRM, billing, entitlements, comms); idempotent; parallel execution |
| Welcome Journey Service | Triggers welcome email sequence; creates in-app onboarding checklist; sends milestone notifications |
| Analytics Service | Records funnel events per step; tracks A/B variant assignments; feeds dashboards |
| Audit Service | Subscribe-only; writes append-only audit records for every state change and decision |
| Customer DB | PostgreSQL — all customer records, onboarding state, verification results, provisioning status |
| Document Store | S3 — uploaded ID documents; encrypted; 1-year minimum retention |
| Kafka | Decouples orchestrator from welcome journey, analytics, and audit |
| Redis | OTP codes (TTL-based); onboarding step cache; provisioning idempotency keys |
Step 4 — Onboarding Lifecycle: End-to-End Flow
The complete journey from a new visitor clicking "Sign Up" to a fully activated, provisioned account.
sequenceDiagram
participant C as Customer
participant ORC as Onboarding Orchestrator
participant VER as Verification Service
participant KYC as KYC/AML Service
participant PROV as Provisioning Service
participant DB as Customer DB
participant KF as Kafka
C->>ORC: POST /v1/onboarding/start {email, password, channel, referral_code}
ORC->>DB: INSERT customer {status: REGISTRATION_STARTED, onboarding_id}
ORC->>VER: SendEmailOTP(email)
ORC-->>C: 201 {onboarding_id, next_step: "verify_email"}
C->>ORC: POST /v1/onboarding/{id}/verify-email {otp_code}
ORC->>VER: VerifyOTP(email, otp_code)
VER-->>ORC: {verified: true}
ORC->>DB: UPDATE step: EMAIL_VERIFIED
ORC-->>C: {next_step: "complete_profile"}
C->>ORC: POST /v1/onboarding/{id}/profile {name, dob, address, phone}
ORC->>VER: SendPhoneOTP(phone)
ORC->>DB: UPDATE step: PROFILE_SUBMITTED
ORC-->>C: {next_step: "verify_phone"}
C->>ORC: POST /v1/onboarding/{id}/verify-phone {otp_code}
ORC->>VER: VerifyOTP(phone, otp_code)
ORC->>DB: UPDATE step: PHONE_VERIFIED
Note over ORC,KYC: KYC — may be async or real-time depending on provider
ORC->>KYC: VerifyIdentity(name, dob, address, id_documents)
KYC-->>ORC: {status: VERIFIED, kyc_ref_id}
ORC->>DB: UPDATE step: KYC_PASSED, status: ACCOUNT_ACTIVATING
ORC->>PROV: ProvisionCustomer(customer_id, plan, channel)
PROV-->>ORC: {crm_id, billing_id, entitlements: [...], provisioned: true}
ORC->>DB: UPDATE customer {status: ACTIVE, provisioned: true}
ORC->>KF: Publish customer.onboarding_completed {customer_id, plan, channel}
ORC-->>C: {status: "active", account_id, welcome_url}
Step 5 — Onboarding State Machine
The onboarding lifecycle is a directed graph of states. Each state represents where the customer is in the process. The orchestrator enforces valid transitions and persists state durably — so if a customer closes the browser mid-onboarding, they resume exactly where they left off.
stateDiagram-v2
[*] --> REGISTRATION_STARTED : Customer submits email + password
REGISTRATION_STARTED --> EMAIL_VERIFICATION_PENDING : OTP sent to email
REGISTRATION_STARTED --> ABANDONED : No activity for 7 days
EMAIL_VERIFICATION_PENDING --> EMAIL_VERIFIED : OTP confirmed
EMAIL_VERIFICATION_PENDING --> EMAIL_VERIFICATION_EXPIRED : OTP expired (no retry within 24h)
EMAIL_VERIFICATION_PENDING --> REGISTRATION_STARTED : Customer requests new OTP
EMAIL_VERIFIED --> PROFILE_COLLECTION : Customer proceeds to profile step
EMAIL_VERIFIED --> ABANDONED : No activity for 7 days
PROFILE_COLLECTION --> PHONE_VERIFICATION_PENDING : Profile submitted, phone OTP sent
PHONE_VERIFICATION_PENDING --> PHONE_VERIFIED : OTP confirmed
PHONE_VERIFIED --> KYC_IN_PROGRESS : KYC check initiated (regulated flows)
PHONE_VERIFIED --> PLAN_SELECTION : Non-regulated flow — skip KYC
KYC_IN_PROGRESS --> KYC_PASSED : Identity verified + AML clear
KYC_IN_PROGRESS --> KYC_FAILED : Identity check failed
KYC_IN_PROGRESS --> KYC_MANUAL_REVIEW : Inconclusive — human review
KYC_MANUAL_REVIEW --> KYC_PASSED : Compliance officer approves
KYC_MANUAL_REVIEW --> KYC_FAILED : Compliance officer rejects
KYC_FAILED --> DECLINED : Regulatory block — account cannot be created
KYC_PASSED --> PLAN_SELECTION : Proceed to plan/product selection
PLAN_SELECTION --> ACCOUNT_ACTIVATING : Plan selected; provisioning initiated
ACCOUNT_ACTIVATING --> ACTIVE : All downstream systems provisioned
ACCOUNT_ACTIVATING --> PROVISIONING_PARTIAL : Some systems provisioned; retry in progress
PROVISIONING_PARTIAL --> ACTIVE : All retries succeeded
PROVISIONING_PARTIAL --> PROVISIONING_FAILED : Retries exhausted — ops intervention needed
ACTIVE --> SUSPENDED : Account suspended (policy violation / payment failure)
ACTIVE --> DELETED : GDPR deletion request fulfilled
ABANDONED --> REGISTRATION_STARTED : Customer returns via re-engagement email
DECLINED --> [*]
DELETED --> [*]
Step Persistence for Resumption
Every step transition is written to the database before the response is sent to the customer. If the service crashes after writing but before responding, the customer retries and the system detects the completed step from the database — not from in-memory state.
-- Atomic step transition — idempotent
INSERT INTO onboarding_steps (onboarding_id, step_name, status, completed_at)
VALUES (:onboarding_id, 'EMAIL_VERIFIED', 'COMPLETED', NOW())
ON CONFLICT (onboarding_id, step_name)
DO UPDATE SET status = EXCLUDED.status, completed_at = EXCLUDED.completed_at
WHERE onboarding_steps.status != 'COMPLETED';
-- Customer sees their current position on any device
SELECT step_name, status, completed_at
FROM onboarding_steps
WHERE onboarding_id = :id
ORDER BY step_order ASC;
Step 6 — Verification Service
Email Verification
sequenceDiagram
participant ORC as Orchestrator
participant VER as Verification Service
participant REDIS as Redis
participant NS as Notification Service
participant C as Customer
ORC->>VER: SendEmailOTP(onboarding_id, email)
VER->>VER: Generate 6-digit OTP
VER->>REDIS: SET otp:email:{onboarding_id} {otp, email} TTL=10min
VER->>NS: SendEmail(email, subject="Verify your email", body="Your code: {otp}")
VER-->>ORC: {sent: true, expires_in: 600}
C->>ORC: POST /verify-email {otp_code: "847291"}
ORC->>VER: VerifyEmailOTP(onboarding_id, "847291")
VER->>REDIS: GET otp:email:{onboarding_id}
REDIS-->>VER: {otp: "847291", email: "[email protected]"}
VER->>VER: Compare submitted vs stored OTP
VER->>REDIS: DEL otp:email:{onboarding_id} (single-use — consume on verify)
VER-->>ORC: {verified: true}
OTP Security Rules
OTP format: 6-digit numeric (1,000,000 possible values)
TTL: 10 minutes
Max attempts: 5 (then OTP is invalidated; must request new one)
Rate limiting: Max 3 OTP sends per email per hour
Max 10 OTP sends per IP per hour
Single-use: OTP deleted from Redis on first successful use
Brute force: After 5 failed attempts → lock OTP + require new one
After 10 failed requests in 1 hour → block IP (temporary)
Phone OTP:
Same rules apply
Additional: carrier-confirmed delivery check (Twilio delivery webhook)
Fallback: Voice call OTP if SMS fails after 2 attempts
Magic Link Alternative
For web flows, a magic link (token-based email verification) improves completion rates by eliminating OTP copy-paste:
Magic link: https://app.example.com/verify?token={signed_jwt}
Token: JWT signed with server private key
Payload: {onboarding_id, email, exp: +10min, nonce: uuid}
Single-use: Token nonce stored in Redis; deleted on use
Expiry: 10 minutes (same as OTP)
Fallback: If magic link not clicked within 10 min → offer OTP instead
Step 7 — KYC/AML Integration
The KYC/AML step applies only to regulated products (financial accounts, lending, crypto). Non-regulated SaaS products skip directly to account activation. The onboarding orchestrator determines which steps are required based on the product type and jurisdiction.
KYC Flow
sequenceDiagram
participant ORC as Orchestrator
participant KYC as KYC/AML Service
participant IDV as Identity Provider\n(Onfido / Jumio / Persona)
participant OFAC as OFAC / PEP API
participant DB as Customer DB
ORC->>KYC: VerifyIdentity(customer_id, {name, dob, address})
KYC->>IDV: CreateVerificationSession(customer_id)
IDV-->>KYC: {session_url: "https://idv.provider.com/session/xyz"}
KYC-->>ORC: {session_url}
ORC-->>Customer: Redirect to IDV provider SDK / iframe
Note over Customer: Customer captures ID document + selfie in IDV flow
IDV-->>KYC: Webhook: verification_completed\n{customer_id, result: APPROVED, confidence: 0.98}
KYC->>OFAC: ScreenName(full_name, dob, nationality)
OFAC-->>KYC: {match: false, score: 0.01}
KYC->>KYC: Compute final KYC decision
KYC->>DB: INSERT kyc_result {customer_id, status: VERIFIED, idv_ref, ofac_score}
KYC-->>ORC: {status: VERIFIED, kyc_result_id}
ORC->>ORC: Transition customer to KYC_PASSED
Step Skipping by Product Type
Financial account (bank, brokerage, lending):
Email verification → Phone verification → KYC/AML → Plan selection → Activation
B2B SaaS (non-regulated):
Email verification → Profile completion → Company verification → Plan selection → Activation
Consumer app (non-regulated):
Email verification → Profile completion → Activation
(KYC skipped entirely)
Crypto / Digital assets:
Email verification → Phone verification → KYC/AML (enhanced) → Accreditation check → Activation
The orchestrator loads a step definition configuration per product type + jurisdiction at startup. Changing the required steps does not require code deployment — only a configuration update.
{
"product_type": "financial_account",
"jurisdiction": "US",
"steps": [
{ "step": "email_verification", "required": true, "order": 1 },
{ "step": "profile_collection", "required": true, "order": 2 },
{ "step": "phone_verification", "required": true, "order": 3 },
{ "step": "kyc_verification", "required": true, "order": 4 },
{ "step": "plan_selection", "required": true, "order": 5 },
{ "step": "account_activation", "required": true, "order": 6 }
]
}
Step 8 — Progressive Profiling
Collecting all customer information at registration causes abandonment. Progressive profiling collects the minimum required data at each step and gathers additional information over time — after the customer has already derived value from the product.
Data Collection Tiers
flowchart TB
T1["Tier 1 — Required at Signup\n(Gate to account creation)\n• Email address\n• Password\n• Full name\n• Date of birth (regulated products)"]
T2["Tier 2 — Required before first use\n(Gate to product activation)\n• Phone number (for 2FA)\n• Home address\n• ID verification (regulated products)\n• Plan selection"]
T3["Tier 3 — Collected post-activation\n(Requested progressively)\n• Payment method\n• Company / employer details\n• Tax information (if applicable)\n• Preferences + communication opt-ins"]
T4["Tier 4 — Optional enrichment\n(Nudged via in-app prompts)\n• Profile photo\n• Job title / industry\n• Use case / goals\n• Referral source"]
T1 --> T2 --> T3 --> T4
Rule: Never request Tier 3 or Tier 4 data during the initial onboarding flow. The customer has not yet experienced the product and has no reason to trust the platform with additional information.
Profile Completion Scoring
Profile completeness score = weighted sum of completed fields
Field weights:
Email verified: 20 points (required)
Phone verified: 15 points (required for regulated)
Full name: 10 points
Address: 10 points
ID verified (KYC): 20 points (required for regulated)
Payment method: 10 points
Company info: 5 points
Profile photo: 5 points
Preferences set: 5 points
Total possible: 100 points
"Complete" threshold: 70 points
Nudge threshold: > 14 days since signup AND score < 70 → trigger profile completion campaign
Step 9 — Account Provisioning
When a customer completes onboarding, the Provisioning Service fans out to all downstream systems in parallel. Each provisioning call is idempotent — a retry cannot create a duplicate record.
Parallel Provisioning Architecture
sequenceDiagram
participant ORC as Orchestrator
participant PROV as Provisioning Service
participant CRM as CRM (Salesforce)
participant BIL as Billing (Stripe)
participant ENT as Entitlements
participant COM as Email Platform
ORC->>PROV: ProvisionCustomer(customer_id, plan, profile)
par Parallel provisioning
PROV->>CRM: CreateContact(customer_id, name, email, plan)
PROV->>BIL: CreateCustomer(customer_id, email, plan_id)
PROV->>ENT: GrantEntitlements(customer_id, plan_features)
PROV->>COM: CreateContactList(customer_id, email, segments)
end
CRM-->>PROV: {crm_id: "00Q5x...", created: true}
BIL-->>PROV: {stripe_customer_id: "cus_xyz", subscription_id: "sub_abc"}
ENT-->>PROV: {entitlements: ["feature_a", "feature_b"], granted: true}
COM-->>PROV: {contact_id: "sg_xyz", lists: ["onboarding", "product_updates"]}
PROV->>DB: UPDATE customer_provisioning\nSET crm_id, billing_id, entitlements_granted=true,\n comm_contact_id, status=COMPLETED
PROV-->>ORC: {all_systems_provisioned: true}
Provisioning Idempotency
Every provisioning call carries an idempotency key derived from the customer ID and system name. If the Provisioning Service crashes mid-fan-out and retries, each downstream system receives the same idempotency key and returns the existing record rather than creating a duplicate.
Idempotency key construction:
CRM: provision:crm:{customer_id}
Billing: provision:billing:{customer_id}
Entitlements: provision:entitlements:{customer_id}:{plan_id}
Comms: provision:comms:{customer_id}
Storage: Redis SET NX {key} {result} TTL=7days
PostgreSQL customer_provisioning table (permanent record)
On idempotency hit:
→ Return cached provisioning result
→ Do not call downstream system again
Partial Provisioning Handling
If some downstream systems fail (e.g., CRM times out but Billing succeeds), the customer is placed in PROVISIONING_PARTIAL state:
flowchart TB
Result["Provisioning Result"]
Result --> AllSuccess{"All systems\nprovisioned?"}
AllSuccess -->|"Yes"| Active["Set status = ACTIVE\nPublish customer.onboarding_completed"]
AllSuccess -->|"No"| Partial["Set status = PROVISIONING_PARTIAL\nRecord which systems failed\nEnqueue retry job"]
Retry["Retry Job\n(exponential backoff: 1m → 5m → 15m → 1h)"]
Retry --> RetryFailed["Re-provision only FAILED systems\n(successful systems already idempotent)"]
RetryFailed --> AllSuccess
MaxRetries["Max retries exceeded (4h)\n→ Set status = PROVISIONING_FAILED\n→ Alert ops team\n→ Customer still partially activated"]
Critical design principle: The customer is not blocked during PROVISIONING_PARTIAL. Their account is activated with whatever was successfully provisioned. Missing systems are retried in the background. The customer is unaware of the partial failure — they see a normal "Welcome" screen.
Step 10 — Welcome Journey Orchestration
The welcome journey is the sequence of communications and in-app experiences that guide a new customer through their first week. It is triggered when onboarding completes and orchestrated by the Welcome Journey Service.
Welcome Journey Timeline
flowchart LR
D0["Day 0 — Activation\n• Welcome email\n• In-app setup checklist\n• Product tour trigger"]
D1["Day 1\n• Check: has customer logged in?\n If yes: feature spotlight email\n If no: re-engagement email"]
D3["Day 3\n• Check: key action completed?\n (e.g., first transaction, first project)\n If yes: congratulations + next step\n If no: tip email + how-to guide"]
D7["Day 7\n• Check: profile completeness < 70?\n If yes: profile completion nudge\n 7-day progress summary email"]
D14["Day 14\n• Upgrade/upsell email\n (if on free tier)\n• Check-in survey"]
D0 --> D1 --> D3 --> D7 --> D14
In-App Onboarding Checklist
The onboarding checklist is a visible, persistent UI element that guides the customer through their first setup actions:
{
"customer_id": "cust_abc123",
"checklist_id": "chk_xyz",
"progress_pct": 60,
"items": [
{ "id": "verify_email", "title": "Verify your email", "completed": true, "completed_at": "2025-03-01" },
{ "id": "add_phone", "title": "Add a phone number", "completed": true, "completed_at": "2025-03-01" },
{ "id": "complete_profile","title": "Complete your profile", "completed": true, "completed_at": "2025-03-01" },
{ "id": "first_action", "title": "Complete your first action", "completed": false, "action_url": "/get-started" },
{ "id": "invite_team", "title": "Invite a team member", "completed": false, "action_url": "/team/invite" }
]
}
Welcome Journey Event Triggers
The Welcome Journey Service subscribes to customer activity events from the product and adjusts the journey in real-time:
| Customer Action | Journey Adjustment |
|---|---|
| Logged in within 24h | Skip re-engagement email; send feature spotlight instead |
| Completed first key action | Send "Congratulations" email; advance checklist |
| Invited a team member | Mark team item complete; send team onboarding trigger |
| Upgraded plan | Switch to advanced user journey; send premium feature guide |
| No login in 7 days | Trigger re-engagement sequence; alert CSM team |
| Profile < 70% at Day 14 | Trigger profile completion campaign |
Step 11 — Abandonment Recovery
60–75% of customers who start onboarding never complete it. Abandonment recovery turns incomplete onboardings into completed accounts.
Abandonment Detection
Definition of abandonment:
No onboarding activity for:
EMAIL_VERIFICATION_PENDING: 24 hours
PROFILE_COLLECTION: 3 days
KYC_IN_PROGRESS: 7 days
PLAN_SELECTION: 3 days
Detection job: runs every 30 minutes
SELECT onboarding_id, current_step, last_activity_at
FROM onboardings
WHERE status NOT IN ('ACTIVE', 'DECLINED', 'ABANDONED', 'DELETED')
AND last_activity_at < NOW() - abandonment_threshold[current_step]
→ Mark as ABANDONED (soft — customer can still resume)
→ Enqueue abandonment recovery sequence
Recovery Sequence
flowchart LR
Abandoned["Customer Abandoned\nat step: PROFILE_COLLECTION"]
H1["Hour 1\nEmail: 'Continue where you left off'\n+ direct link to current step"]
H24["24 Hours\nEmail: 'We saved your progress'\n+ benefit reminder"]
D3["Day 3\nEmail: 'Still thinking it over?'\n+ social proof / testimonials"]
D7["Day 7\n'We noticed you haven't completed'\n+ offer: 1-month free trial"]
D14["Day 14\nFinal email\n'Your account will expire in 7 days'"]
D21["Day 21\nSoft delete — mark as EXPIRED\nData retained 30 days then purged"]
Abandoned --> H1 --> H24 --> D3 --> D7 --> D14 --> D21
Resume["Customer clicks link\n→ Resumes exactly at current step\n→ Recovery sequence cancelled"]
H1 --> Resume
H24 --> Resume
D3 --> Resume
Step 12 — Database Design
Core Tables
-- Master onboarding record
CREATE TABLE onboardings (
onboarding_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
customer_id UUID, -- NULL until account is created
email VARCHAR(255) NOT NULL,
channel VARCHAR(20) NOT NULL, -- 'WEB' | 'MOBILE' | 'PARTNER_API' | 'AGENT'
product_type VARCHAR(30) NOT NULL,
jurisdiction CHAR(2) NOT NULL DEFAULT 'US',
status VARCHAR(30) NOT NULL DEFAULT 'REGISTRATION_STARTED',
current_step VARCHAR(50) NOT NULL DEFAULT 'EMAIL_VERIFICATION',
ab_variant VARCHAR(20), -- A/B test variant assigned at start
referral_code VARCHAR(50),
utm_source VARCHAR(100),
idempotency_key VARCHAR(255) UNIQUE,
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Step-level completion tracking
CREATE TABLE onboarding_steps (
step_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
onboarding_id UUID NOT NULL REFERENCES onboardings(onboarding_id),
step_name VARCHAR(50) NOT NULL,
step_order INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING | IN_PROGRESS | COMPLETED | SKIPPED | FAILED
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
attempts INT NOT NULL DEFAULT 0,
metadata JSONB, -- step-specific data (OTP attempt count, KYC ref, etc.)
UNIQUE (onboarding_id, step_name)
);
-- Customer master record (created at activation)
CREATE TABLE customers (
customer_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
onboarding_id UUID NOT NULL REFERENCES onboardings(onboarding_id),
email VARCHAR(255) NOT NULL UNIQUE,
phone VARCHAR(20),
full_name VARCHAR(200) NOT NULL,
date_of_birth DATE,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
kyc_status VARCHAR(20) NOT NULL DEFAULT 'NOT_REQUIRED',
profile_score INT NOT NULL DEFAULT 0,
plan_id VARCHAR(50),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Downstream provisioning status
CREATE TABLE customer_provisioning (
provisioning_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
customer_id UUID NOT NULL REFERENCES customers(customer_id),
system_name VARCHAR(30) NOT NULL, -- 'CRM' | 'BILLING' | 'ENTITLEMENTS' | 'COMMS'
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
external_id VARCHAR(200), -- ID in the downstream system
idempotency_key VARCHAR(255) NOT NULL UNIQUE,
attempt_count INT NOT NULL DEFAULT 0,
last_attempted_at TIMESTAMPTZ,
provisioned_at TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (customer_id, system_name)
);
-- Immutable onboarding event log
CREATE TABLE onboarding_events (
event_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
onboarding_id UUID NOT NULL REFERENCES onboardings(onboarding_id),
event_type VARCHAR(50) NOT NULL,
from_status VARCHAR(30),
to_status VARCHAR(30),
step_name VARCHAR(50),
actor_type VARCHAR(20) NOT NULL DEFAULT 'SYSTEM',
actor_id VARCHAR(100),
metadata JSONB,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- In-app onboarding checklist
CREATE TABLE onboarding_checklists (
checklist_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
customer_id UUID NOT NULL REFERENCES customers(customer_id) UNIQUE,
total_items INT NOT NULL,
completed_items INT NOT NULL DEFAULT 0,
progress_pct INT GENERATED ALWAYS AS
(CASE WHEN total_items = 0 THEN 0
ELSE (completed_items * 100 / total_items) END) STORED,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE checklist_items (
item_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
checklist_id UUID NOT NULL REFERENCES onboarding_checklists(checklist_id),
item_key VARCHAR(50) NOT NULL,
title VARCHAR(200) NOT NULL,
action_url VARCHAR(500),
completed BOOLEAN NOT NULL DEFAULT FALSE,
completed_at TIMESTAMPTZ,
UNIQUE (checklist_id, item_key)
);
Key Indexes
-- Lookup onboarding by email (resumption flow)
CREATE INDEX idx_onboarding_email ON onboardings(email, status);
-- Active onboardings by step (queue monitoring)
CREATE INDEX idx_onboarding_step ON onboardings(current_step, status)
WHERE status NOT IN ('ACTIVE', 'DECLINED', 'ABANDONED', 'DELETED');
-- Abandonment detection scan
CREATE INDEX idx_onboarding_activity ON onboardings(last_activity_at, status)
WHERE status NOT IN ('ACTIVE', 'DECLINED', 'ABANDONED', 'DELETED');
-- Provisioning retry queue
CREATE INDEX idx_provisioning_retry ON customer_provisioning(status, last_attempted_at)
WHERE status IN ('PENDING', 'FAILED');
-- Events per onboarding (timeline)
CREATE INDEX idx_events_onboarding ON onboarding_events(onboarding_id, occurred_at DESC);
-- Steps by onboarding (resumption — load current position)
CREATE INDEX idx_steps_onboarding ON onboarding_steps(onboarding_id, step_order);
Step 13 — API Design
Start Onboarding
POST /v1/onboarding/start
{
"idempotency_key": "reg-device-D123-2025-03-01T10:00:00Z",
"email": "[email protected]",
"password": "••••••••",
"channel": "web",
"product_type": "financial_account",
"referral_code": "FRIEND2025",
"utm_source": "google_ads"
}
201 Created:
{
"onboarding_id": "onb_9f8e7d6c",
"status": "email_verification_pending",
"next_step": {
"step": "verify_email",
"instruction": "Enter the 6-digit code sent to [email protected]",
"expires_in": 600
},
"created_at": "2025-03-01T10:00:02Z"
}
Resume Onboarding
GET /v1/onboarding/{onboarding_id}/status
{
"onboarding_id": "onb_9f8e7d6c",
"status": "profile_collection",
"progress_pct": 37,
"current_step": {
"step": "complete_profile",
"fields_required": ["phone", "date_of_birth", "home_address"],
"fields_optional": ["job_title"]
},
"completed_steps": ["email_verification", "password_set"],
"remaining_steps": ["complete_profile", "verify_phone", "kyc_verification", "plan_selection"]
}
Complete a Step
POST /v1/onboarding/{onboarding_id}/steps/{step_name}
{
"step_name": "profile_collection",
"data": {
"full_name": "Alex Johnson",
"date_of_birth": "1990-05-15",
"phone": "+14155550100",
"address": {
"line1": "123 Main St", "city": "San Francisco",
"state": "CA", "zip": "94105", "country": "US"
}
}
}
200 OK:
{
"step_name": "profile_collection",
"status": "completed",
"next_step": {
"step": "verify_phone",
"instruction": "Enter the 6-digit code sent to +1 (415) 555-0100"
},
"progress_pct": 62
}
Activate Account (Final Step)
POST /v1/onboarding/{onboarding_id}/activate
{
"plan_id": "plan_starter",
"accept_terms": true,
"accepted_at": "2025-03-01T10:05:00Z"
}
200 OK:
{
"customer_id": "cust_abc123",
"status": "active",
"account_url": "https://app.example.com/dashboard",
"welcome_message": "Welcome to Example! Your account is ready.",
"checklist_url": "https://app.example.com/setup",
"activated_at": "2025-03-01T10:05:12Z"
}
Step 14 — Kafka Event Architecture
flowchart TB
ORC["Onboarding Orchestrator"]
ORC --> E1["onboarding.started"]
ORC --> E2["onboarding.step_completed"]
ORC --> E3["onboarding.kyc_completed"]
ORC --> E4["onboarding.abandoned"]
ORC --> E5["onboarding.completed"]
ORC --> E6["customer.provisioned"]
E1 --> ANL["Analytics\n(funnel tracking)"]
E1 --> AuditS["Audit Service"]
E2 --> ANL
E2 --> AuditS
E3 --> ANL
E3 --> AuditS
E4 --> WJ["Welcome Journey\n(abandonment recovery sequence)"]
E4 --> ANL
E5 --> WJ2["Welcome Journey\n(welcome email + checklist)"]
E5 --> ANL
E5 --> AuditS
E5 --> PROV["Provisioning Service\n(fan-out to downstream systems)"]
E6 --> NS["Notification\n('Your account is ready')"]
E6 --> ANL
Step 15 — Observability and Failure Handling
Funnel Analytics Dashboard
The most important visibility tool for onboarding is the step-by-step conversion funnel. Every step drop-off is an opportunity to improve:
| Step | Entered | Completed | Drop-off Rate | Avg Time |
|---|---|---|---|---|
| Registration start | 100,000 | 100,000 | 0% | — |
| Email verification | 100,000 | 87,200 | 12.8% ⚠️ | 3.2 min |
| Profile collection | 87,200 | 71,300 | 18.2% ⚠️ | 4.8 min |
| Phone verification | 71,300 | 68,400 | 4.1% | 2.1 min |
| KYC verification | 68,400 | 59,600 | 12.9% ⚠️ | 8.4 min |
| Plan selection | 59,600 | 55,100 | 7.5% | 2.6 min |
| Activation | 55,100 | 55,100 | — | — |
| Overall conversion | 100,000 | 55,100 | 44.9% | 21 min avg |
High drop-off at email verification (12.8%) signals OTP delivery problems or too-short expiry. High drop-off at profile collection (18.2%) signals too many required fields. High drop-off at KYC (12.9%) signals friction in the document capture flow.
Key Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
onboarding.registration_success_rate |
% registrations that start successfully | < 99% |
onboarding.overall_completion_rate |
% started → activated | < 50% (structural UX issue) |
onboarding.step_drop_off_rate |
Per-step abandonment rate | > 20% for any single step |
onboarding.avg_time_to_activation_min |
Mean time from start to ACTIVE | > 30 minutes |
onboarding.kyc_auto_approval_rate |
% KYC auto-approved without manual review | < 85% (IDV provider issue) |
provisioning.failure_rate |
% provisioning calls failing | > 2% |
provisioning.partial_rate |
% customers stuck in PROVISIONING_PARTIAL > 1h | > 1% |
otp.delivery_success_rate |
% OTPs delivered successfully | < 98% |
onboarding.abandonment_recovery_rate |
% abandoned customers who return | Track weekly |
kafka.consumer_lag |
Lag on onboarding event topics | > 5,000 messages |
CAT Event Handling (Viral Campaign Spikes)
A promotional campaign or viral moment can drive 10× normal registration volume in minutes:
flowchart LR
Spike["10× Registration Spike\n(campaign launch or viral post)"]
Spike --> KF["Kafka\nRegistration topic\n(absorbs burst — never drops)"]
KF --> Workers["Onboarding Workers\n(auto-scale on consumer lag)"]
Workers --> DB["PostgreSQL\n(all registrations persisted)"]
Spike --> KYC_Queue["KYC Queue\n(IDV provider has rate limits)\n→ Queue KYC requests\n→ Process at IDV rate limit"]
Spike --> PROV_Queue["Provisioning Queue\n(CRM/billing rate limits)\n→ Batch provisioning\n→ Customer sees 'activating' screen"]
During spikes, the customer's onboarding experience remains responsive (registration completes instantly) while KYC and provisioning process from a queue. The customer sees intermediate status pages ("We're setting up your account") rather than slow responses.
Design Trade-offs
Trade-off 1: Monolithic Onboarding Flow vs Step-by-Step API
| Approach | Pros | Cons |
|---|---|---|
| Step-by-step API (chosen) — each step is a separate API call | Resumable across sessions and devices; step state is durable; fine-grained analytics per step | More round trips; more complex state management |
| Single form submission | One API call; simpler frontend | No resumption; entire form lost on failure or abandonment; no step-level analytics |
Decision: Step-by-step API. The #1 cause of onboarding abandonment is users unable to resume after an interruption. A monolithic form is unsalvageable if the customer gets a phone call mid-fill. Step-by-step state persistence is the foundation of a high-converting onboarding flow.
Trade-off 2: Synchronous vs Asynchronous Provisioning
| Approach | Pros | Cons |
|---|---|---|
| Asynchronous provisioning (chosen) | Customer sees "Welcome" immediately; provisioning failures don't block activation | Customer may try to use a feature before all systems are provisioned |
| Synchronous provisioning | Customer is guaranteed fully provisioned before seeing welcome screen | Adds 2–10 seconds to activation; any downstream timeout blocks activation entirely |
Decision: Asynchronous with graceful degradation. The customer is activated immediately and shown the welcome screen. If a downstream system is not yet provisioned, the feature shows a loading state or graceful "coming soon" message. Background retries complete provisioning within minutes. A synchronous approach would make the activation time dependent on the slowest downstream system.
Trade-off 3: Minimal Upfront Data vs Complete Profile at Signup
| Approach | Pros | Cons |
|---|---|---|
| Progressive profiling (chosen) | Higher conversion; lower abandonment; trust built before data requested | Profile may be incomplete at activation; some features gated until profile complete |
| Complete profile at signup | Customer fully profiled from day one; no follow-up collection needed | High abandonment; customers don't trust new platforms with all data upfront |
Decision: Progressive profiling always. Industry data consistently shows shorter signup forms double conversion rates. Collecting additional data over time, after the customer has derived value, is far more effective than collecting it all upfront.
Trade-off 4: Single Welcome Email vs Journey Orchestration
| Approach | Pros | Cons |
|---|---|---|
| Journey orchestration (chosen) | Behavior-based; adapts to what the customer actually does; higher engagement | More complex; requires activity event tracking; needs a journey orchestration system |
| Single welcome email | Simple; no infrastructure needed | One-size-fits-all; no adaptation to customer behavior; lower activation rate |
Decision: Journey orchestration. A single welcome email is a commodity. A behavior-adaptive welcome journey that nudges the customer based on what they have and haven't done produces measurably higher activation, retention, and first-week engagement. The infrastructure cost is justified by the conversion impact.
Common Interview Mistakes
| Mistake | Why It Matters |
|---|---|
| Making onboarding a single synchronous API call | No resumption on failure; entire flow lost if any step fails; no step-level analytics |
| Not persisting step state to the database | Onboarding state in memory is lost on server restart; customer must restart from beginning |
| Synchronous provisioning blocking activation | If Salesforce is down, no customers can activate — downstream dependency becomes a hard blocker |
| Missing idempotency on provisioning calls | Retry after a provisioning timeout creates duplicate CRM contacts, duplicate billing subscriptions |
| Collecting all profile data at signup | Industry data shows this is the #1 cause of abandonment; progressive profiling is not optional for high conversion |
| No abandonment recovery flow | 60%+ of customers will abandon; without recovery, that's permanent revenue loss |
| Ignoring KYC step skipping by product type | Applying KYC to products that don't require it (B2B SaaS, consumer apps) creates unnecessary friction and drop-off |
| No funnel analytics per step | Without step-level drop-off data, product teams cannot identify where to improve the flow |
Summary
The Customer Onboarding System is one of the highest-leverage systems in any customer-facing business. It is the first real product experience after signup, the foundation for regulatory compliance, and the primary driver of early customer activation.
flowchart TB
Core["Customer Onboarding System"]
Core --> StateMachine["Durable State Machine\nStep persistence\nCross-session resumption"]
Core --> Verification["Verification Service\nEmail + Phone OTP\nMagic link option"]
Core --> KYC_Block["KYC/AML\nProduct-type gated\nAsync IDV webhook"]
Core --> Progressive["Progressive Profiling\nMinimal upfront data\nTiered collection"]
Core --> Provisioning["Parallel Provisioning\nIdempotent fan-out\nPartial failure tolerance"]
Core --> Journey["Welcome Journey\nBehavior-adaptive\nAbandonment recovery"]
The four design principles that make onboarding convert:
-
State is durable, not in-memory — every step completion is written to the database before the response is sent. A customer who closes their browser mid-onboarding resumes exactly where they left off, on any device, days later.
-
Provisioning never blocks activation — the customer sees a welcome screen the moment their account is created. Downstream system provisioning happens asynchronously in the background with idempotent retries. The customer experience is decoupled from infrastructure reliability.
-
Ask only what is needed now — progressive profiling is not a compromise; it is the scientifically validated approach to maximizing completion rates. Every additional required field at signup costs conversion.
-
Measure every step — without step-level funnel analytics, product teams operate blind. The onboarding funnel dashboard is the primary diagnostic tool for identifying where customers are lost and why.