Applied Patterns — Onboarding Platform Playbooks
Concrete platform pattern playbooks an interviewer might design-probe. Each one is the kind of thing a senior PM should be able to whiteboard with an eng partner — the surface area, the trade-offs, and the failure modes.
Pattern 1 — Vendor abstraction layer for IDV providers
Problem: Today, calls to Persona (or whichever) are scattered across services. Swapping or running two vendors in parallel requires a release.
Pattern: A platform-owned IdentityVerificationProvider interface with concrete adapters per vendor (PersonaAdapter, JumioAdapter, OnfidoAdapter). App code only talks to the interface.
# Conceptual interface — what the platform exposes upward
class IdentityVerificationProvider:
def create_session(self, applicant: Applicant, flow: FlowConfig) -> SessionToken: ...
def get_decision(self, session: SessionToken) -> Decision: ...
def fetch_evidence(self, session: SessionToken) -> Evidence: ...
def cancel(self, session: SessionToken) -> None: ...
# Decision is a normalized schema — every adapter maps the vendor's
# native response into this shape, so the platform decision-engine
# doesn't care which vendor responded.
@dataclass
class Decision:
outcome: Literal["approve", "refer", "reject"]
confidence: float # 0-1, normalized across vendors
signals: dict # vendor-native signal payload (audit only)
evidence_urls: list[str] # links into the audit store
vendor: str
decision_id: str # idempotency / replay key
Trade-offs to discuss:
- Lowest-common-denominator risk: vendor-specific capabilities (e.g. Onfido's age estimation) don't fit the interface. Solution: extend the schema additively with optional fields, gated by capability flags.
- Score normalization is non-trivial — you may need a small calibration model per vendor.
- Evidence shape differs by vendor (Persona's JSON, Jumio's PDF). Store both raw and normalized.
What the platform unlocks: A/B test vendors against each other in the same flow. Renegotiate contracts with credible exit. Failover during outage. Compliance reviews the abstraction once, not per vendor.
Pattern 2 — Regional onboarding rule engine
Problem: Which checks run, in what order, at what tier, differs per jurisdiction. Encoding this in app code locks Compliance out of the loop.
Pattern: A versioned, declarative policy document per (jurisdiction, segment, tier). Compliance edits the doc; the platform interprets it at runtime.
// /policy/DE/consumer/tier-2.v3.json
{
"policy_id": "de-consumer-t2-v3",
"effective_from": "2026-01-15",
"approved_by": "compliance-DE-lead",
"required_steps": [
{"step": "collect_personal_data", "fields": ["name", "dob", "address", "nationality"]},
{"step": "verify_document", "doc_types": ["passport", "national_id_de", "residence_permit"]},
{"step": "liveness_check", "level": "active"},
{"step": "sanctions_screen", "lists": ["OFAC", "EU", "UN", "DE_national"]},
{"step": "pep_screen", "threshold": "tier_1_and_above"},
{"step": "address_proof", "doc_types": ["utility_bill", "bank_statement"], "max_age_days": 90}
],
"decision_rules": [
{"if": "sanctions.hit", "then": "block_and_escalate"},
{"if": "pep.match", "then": "refer_to_edd"},
{"if": "liveness.confidence < 0.9", "then": "retry_or_manual"}
]
}
Discussion points:
- Versioning is mandatory — every applicant's record cites the policy version they were onboarded under. Regulator question "what did your KYC look like for customers onboarded in March 2025?" becomes a query.
- The policy doc is the system of record. Compliance change → PR → CI validates schema → Compliance signs off → deploy. No code change.
- Effective-dates allow scheduled rollouts of policy changes.
- The policy DSL stays narrow on purpose. Don't recreate a Turing-complete rules engine — make it readable by a compliance lead.
Pattern 3 — Audit-event firehose
Problem: Regulators ask "what happened to applicant X on date Y." Reconstructing this from logs, sessions, and vendor portals is a project. Audit defensibility must be a feature, not a salvage.
Pattern: Every state transition emits a structured event to an append-only store. Events are immutable, versioned, and queryable.
{
"event_id": "evt_01H9...",
"event_type": "verification.decision_made",
"schema_version": "v1",
"occurred_at": "2026-03-14T09:21:44.123Z",
"applicant_id": "appl_01H...",
"jurisdiction": "DE",
"policy_version": "de-consumer-t2-v3",
"actor": {"type": "system", "service": "decision-engine", "version": "2.4.1"},
"payload": {
"decision": "approve",
"confidence": 0.97,
"vendor": "persona",
"signals_summary": {...},
"evidence_refs": ["s3://audit/appl_01H.../persona-evidence.json"]
},
"correlation_id": "session_01H...",
"causation_id": "evt_01H7..."
}
Discussion points:
- Schema versioned per event type; new fields added additively. Old events stay valid.
- Retention policy aligns to the strictest applicable regulatory window (typically 5-7 years).
- PII in events is encrypted at rest with key-per-applicant — supports legitimate erasure where AML doesn't override.
- Downstream consumers: ops console, compliance reports, ML risk-scoring, fraud team. All read; none mutate.
- Causation_id chains let you replay a full applicant journey.
Pattern 4 — Self-serve flow builder
Problem: Futures wants a tier-upgrade flow specific to derivatives suitability. Doing it requires platform-team eng. The team is a bottleneck.
Pattern: Downstream teams configure flows by composing platform primitives, in a config repo owned by them. Platform CI validates that the composition is legal under platform constraints and applicable policy.
- Primitives:
verify_id,screen,collect_consent,collect_suitability,set_tier,emit_audit_event. - Composition: a sequence of primitives with conditional branches.
- Validation: platform CI checks that the resulting flow satisfies the relevant jurisdictional policy (a flow that omits sanctions screening in DE fails CI).
- Governance: compliance review on the flow definition is fast because the surface is small and structured.
Phasing matters. Don't ship the flow builder until the primitives are stable. Premature builders create migration nightmares when primitives change.
Self-serve doesn't mean ungoverned. The platform's job is to make the right thing the easy thing. Constrain the space to legal compositions; let the consumer team express intent within it.
Pattern 5 — Tier-upgrade with grandfathering
Problem: You've made tier 2 stricter (e.g. now requires source-of-funds). 800k existing tier-2 customers exist. Do they need to redo? Some, but not all.
Pattern: The tier system tracks policy version per applicant. Upgrading the tier definition does not auto-retroactively change existing applicants. Compliance decides which cohort needs to re-verify, on what schedule, with what user experience.
- Every applicant carries
(tier, policy_version, last_verified_at). - A re-KYC campaign is a query: "applicants where policy_version < X and tier ≥ 2."
- The platform supports a "soft-block" — restrict capability without forcing redo, e.g. allow trading but block new deposits.
- The platform supports a "step-up" — when an applicant tries to do something requiring the new tier, walk them through the delta only, not the full flow.
This is one of the most operationally important platform patterns — gets used every time a regulator updates expectations.
Pattern 6 — Sandbox / test mode for downstream teams
Problem: Downstream teams need to integrate without burning real vendor cost or polluting compliance logs.
Pattern: A sandbox that exercises the same shapes (idempotency, errors, events, latency) without making real vendor calls. Deterministic test fixtures for common scenarios.
- Magic test inputs return canned outcomes:
name="Test Approved"always approves;name="Test Sanctions Match"always blocks. - Latency simulation toggleable (real vendors are not instant; sandbox must let teams test loading states).
- Sandbox events emit on a separate firehose, never co-mingled with production audit.
- Each downstream team has a "scenario library" they can extend.
The sandbox is a deliverable. If downstream teams can't integrate without your help, you have not yet shipped a platform.
Pattern 7 — Identity resolution: applicant_id vs person_id
Problem: A user signs up, abandons, comes back six months later with a new email. Or onboards as consumer, then their company tries to KYB and the same person is a beneficial owner. Same human; different records.
Pattern: Three-level identity hierarchy:
- person_id — the resolved real-world person/entity. Stable. Backed by identity-resolution rules (verified gov-ID + DOB matches an existing person → merge).
- applicant_id — a particular onboarding attempt for a particular product / segment.
- user_id / account_id — application-level identity, tied to an account.
Each level has its own lifecycle. Implications:
- Sanctions screening operates at person_id — you want one hit to surface across all the person's accounts.
- Tier upgrade for one segment shouldn't force re-verification for another segment if the underlying person is already verified.
- Identity-resolution disputes are expensive — design for false-merge avoidance (manual review required to merge confidently).
How these patterns show up in interviews
You'll be asked questions like:
- "Design the platform that lets Futures launch a new tier in a new country in a week."
- "Walk me through how you'd handle adding a new IDV vendor."
- "A regulator asks how your KYC controls have changed over the last 12 months. What's your evidence?"
- "Compliance wants to roll back a policy change to all customers verified after a date. How does that work?"
Use the patterns above as composable building blocks. The strong answer always shows the architecture, the unit-of-change, the audit story, and the consumer-team experience.
For any platform design question: (1) Name the customer (which internal team). (2) Sketch the primitive(s) — what's the API/config. (3) State what's versioned and how. (4) State the audit shape. (5) Name two failure modes and how the platform handles them. (6) State the metric you'd track to know it's working.