System Design Interview
Design a Membership Benefits & Verification Platform
100K+ members across 9 organisations — one API to verify membership, personalise benefits, and drive engagement without spamming
The Interview Question
"Design a platform that serves 118,000 members across 9 independent organisations. Providers (banks, insurers, retailers) query your API to verify membership at checkout. Members need to discover benefits relevant to their situation, receive timely notifications without being spammed, and manage their preferences. How do you build this in a way that keeps all three parties — members, organisations, and providers — satisfied?"
This question tests multi-tenancy design, API gateway patterns, notification throttling, and how to drive engagement without burning out your audience. It's a real-world product-engineering problem, not just infrastructure.
Step 1: Requirements
Functional
- Provider API: verify that a given user_id or email is an active member of a given organisation
- Member portal: browse available benefits, track usage, manage preferences, register dependents
- White-label mobile app: each organisation publishes under its own brand; content is filtered by organisation
- Push notifications: both the platform and individual organisations can send notifications to their members
- Organisation portal: organisations see their members' benefit usage reports and can publish articles/notifications
Non-functional
- 118,000 active members across 9 organisations
- Provider API must respond in < 200ms (used at POS/checkout — synchronous call)
- Notifications: never more than 3 push notifications per week per member
- Member data is PII — each organisation owns its own members; the platform must never cross-wire them
- White-label apps: 9 apps in the stores, all backed by the same API, distinguishable only by tenant context
Step 2: Multi-Tenancy — The Core Design Decision
Nine organisations share one platform, but members must never see each other's data across organisations. This is the multi-tenancy problem, and there are three standard approaches:
Approach 1: Separate database per organisation
✓ Strong isolation — a bug in one org cannot leak to another
✓ Independent backups, migrations, scaling
✗ 9× the operational overhead
✗ Cross-org analytics (platform-level reports) require query federation
Approach 2: Shared database, separate schema per organisation
✓ Easier cross-org reporting
✓ One DB to manage, with schema-level isolation
✗ Schema-per-tenant complicates migrations (9× migration scripts)
✗ Moderately higher blast radius on misconfiguration
Approach 3: Shared database, shared tables, tenant_id column everywhere
✓ Simplest to operate — one schema
✓ Cross-org analytics trivial
✗ Must enforce tenant_id filter on EVERY query — easy to miss, catastrophic if missed
✗ Row-level security (RLS) in PostgreSQL mitigates this but requires careful setupRecommended approach for this scale: shared database + shared schema + PostgreSQL Row-Level Security.
At 118,000 members across 9 organisations, the data volume is small. Operational simplicity wins. PostgreSQL RLS enforces tenant isolation at the database layer so application bugs cannot accidentally expose another org's members.
-- Every table has a tenant column
ALTER TABLE members ENABLE ROW LEVEL SECURITY;
CREATE POLICY member_isolation ON members
USING (organisation_id = current_setting('app.current_org')::uuid);
-- Application sets this before every query
SET LOCAL app.current_org = '{{ organisation_id }}';Step 3: The Verification API
Providers call this API to confirm membership at checkout. The latency requirement is strict (< 200ms) because this is a synchronous call in a payment flow.
Provider request:
GET /v1/verify
Authorization: Bearer {provider_api_key}
X-Organisation-Id: {org_id}
Body: { "email": "member@example.com" }
Response (200ms SLA):
{ "verified": true, "member_since": "2021-03-15", "tier": "full" }
OR
{ "verified": false }What makes this fast:
-
API key → organisation context: The provider's API key is pre-associated with one organisation. No org_id lookup needed per request — set at key issuance.
-
Redis membership cache: Member verification is pure read. The most recent member list from each organisation is cached in Redis with a 5-minute TTL.
Key: verify:{org_id}:{email_hash}
Value: { verified: true, tier: "full", member_since: "..." }
TTL: 300 seconds- Cache miss fallback: Query PostgreSQL with the RLS context set. Write result to Redis. Return.
Typical flow:
Redis HGET → HIT in ~0.5ms → return 200 OK (total: ~10ms with network)
Cache miss:
Redis → miss → PostgreSQL query (~5ms) → write to Redis → return (~20ms)
Both paths comfortably within 200ms SLA.- Organisation pushes updates to the platform: When an organisation adds or removes a member, they call:
POST /v1/members/sync
Body: { "members": [...] } ← delta or full listThis updates PostgreSQL and invalidates the Redis cache for affected members.
Step 4: Notification Architecture — Solving the Fatigue Problem
This is the most interesting design problem in the question. You have three parties that want to send notifications:
- The platform (benefit announcements, app updates)
- Each of the 9 organisations (their own editorial content)
- Providers (promotional campaigns — this should be mediated)
And one constraint: members must not be over-notified.
The naive model fails:
Platform sends 1 notification/week to all orgs' members
Each of 9 orgs sends 1 notification/week to their members
= 10 notifications/week per member ← inbox fatigue, app uninstallsSolution: a notification budget per member, enforced centrally
NOTIFICATION_BUDGET table:
member_id UUID
channel ENUM (push, email, sms)
week_start DATE
count_used INT
max_allowed INT (default: 3 push / week)
Before any notification is sent:
SELECT count_used, max_allowed
FROM notification_budget
WHERE member_id = ? AND channel = 'push' AND week_start = current_week
IF count_used >= max_allowed:
→ enqueue to "low_priority" queue → deliver next week
ELSE:
→ send → increment count_usedPriority tiers:
Priority 1 (always send, doesn't consume budget):
- Security alerts (password changed, suspicious login)
- Transactional (order confirmation, benefit redeemed)
Priority 2 (counts against budget):
- Platform benefit announcements
- Organisation editorial (new article, event)
Priority 3 (only if budget available, else drop):
- Marketing / promotional
- Weekly remindersOrganisations submit notifications via their portal. The notification service evaluates priority, checks the member's budget and preferences, and either dispatches or queues.
Step 5: Personalisation — Relevant Benefits, Not Noise
The platform has a broad benefit catalogue — bank discounts, travel perks, gym memberships, professional development. A doctor and a recent graduate have very different benefit relevance.
Member profile dimensions:
- Organisation (which org they belong to)
- Profession (from org-supplied member data)
- Life stage (inferred from benefit usage patterns)
- Explicit preferences (categories they've bookmarked or hidden)
- Recent usage (what they've actually redeemed)
Benefit relevance score:
score = base_score
+ category_match × 0.4 (profession-to-category relevance)
+ usage_history × 0.3 (categories they've redeemed before)
+ organisation_weight × 0.2 (org has promoted this benefit)
+ recency_boost × 0.1 (new benefit added this week)For 118,000 members, this scoring can run as a nightly batch job (not real-time):
Nightly at 02:00:
For each active member:
Fetch benefit catalogue (150 benefits)
Compute relevance scores
Store top 20 recommendations → Redis: recs:{member_id}
Member opens app:
GET /v1/feed → Redis LRANGE recs:{member_id} 0 19
→ <1ms, fully personalisedAt 118,000 members and 150 benefits, this is 17.7 million score computations — trivially fast on a single batch worker.
Step 6: White-Label App Architecture
9 apps in the store, one backend. The key design question is: where does tenancy live?
Option A: One API, tenant from JWT
The member's JWT contains { member_id, org_id }
API routes filter all content by org_id
Option B: Subdomain per tenant
legeforeningen.benefitsapp.com vs tannlegeforeningen.benefitsapp.com
API reads subdomain → sets org context
Recommended: Option A (JWT-based tenancy)
- Simpler DNS (one domain)
- Tenant context is cryptographically bound to the token
- No ambiguity if a member is ever in two organisationsThe white-label app is a React Native app with a build-time configuration per organisation:
app.config.{org_slug}.json:
{
"org_id": "...",
"theme": { "primary": "#1a5f7a", "logo": "legeforeningen_logo.png" },
"app_name": "A+ Legeforeningen",
"push_sender_id": "..."
}The backend is identical for all 9 apps. The frontend renders org-specific branding from the config and filters content via the JWT org_id claim.
Step 7: Full Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ White-label Apps (×9) Member Portal Org Portal │
└────────────┬────────────────────────┬─────────────────┬─────────────┘
│ │ │
┌────────────▼────────────────────────▼─────────────────▼─────────────┐
│ API Gateway │
│ Auth (JWT) · Rate limiting · Tenant context injection │
└────┬────────────┬────────────────┬──────────────────┬────────────────┘
│ │ │ │
┌────▼───┐ ┌─────▼──────┐ ┌──────▼──────┐ ┌───────▼───────┐
│Verify │ │ Member │ │Notification │ │ Benefits & │
│API │ │ Service │ │ Service │ │ Content Svc │
└────┬───┘ └─────┬──────┘ └──────┬──────┘ └───────┬───────┘
│ │ │ │
┌────▼────────────▼────────────────▼──────────────────▼────────────────┐
│ PostgreSQL (RLS-enforced multi-tenant) │
│ Redis (verify cache · notification budget · feed recs) │
│ Blob Storage (article images · documents · logos) │
└────────────────────────────────────────────────────────────────────────┘
▲
┌──────────┘
│ Org data sync API
│ (organisations push member updates)
┌─────────┴────────────────┐
│ 9 Organisations │
│ (member databases) │
└───────────────────────────┘Step 8: Data Ownership — A Governance Question
This platform handles PII for members of professional associations. A few non-obvious design constraints:
The platform does not own member data — organisations do. The platform stores member records only as long as the organisation's contract is active. If an organisation leaves, all their members' data must be deletable on demand (GDPR Article 17).
Implementation:
soft_delete = true per member on org offboarding
Hard deletion job runs after 30-day retention period
Redis TTL auto-expires cache entries
S3 lifecycle policy deletes member-uploaded documents after 7 yearsMembers choose what to share. Consent is granular: benefit usage history, personalisation profile, marketing communications. The consent table drives what data the platform can process:
MEMBER_CONSENTS
member_id UUID
consent_type ENUM (usage_tracking, personalisation, marketing)
granted BOOLEAN
granted_at TIMESTAMPTZ
revoked_at TIMESTAMPTZ (nullable)Providers see only verification results, never member data. Providers cannot query "give me all members of org X". They can only ask "is this specific person a member?" — a binary response with no additional PII.
What the Interviewer Is Actually Testing
- Do you identify multi-tenancy as the core structural problem and compare isolation strategies?
- Can you design a verification API that meets a sub-200ms SLA at checkout?
- Do you propose a notification budget system to prevent inbox fatigue across multiple senders?
- Do you address personalisation as a nightly batch problem, not a real-time one (right tool, right scale)?
- Do you handle white-label tenancy at the JWT level rather than duplicating infrastructure?
- Do you raise data ownership and GDPR as design constraints, not afterthoughts?
- Do you recognise that at 118,000 members, operational simplicity beats distributed system complexity?
Related Case Studies
Go Deeper
Case studies teach the "what". Our courses teach the "how" — the patterns behind these decisions, built up from first principles.
Explore Courses