CASE STUDY

Buy Now Pay Later (BNPL) System (Machine Coding)

2 min read·391 words·Intermediate

Asked at

1 candidate report in Aug 2026

How to use this case study

SDE-2 / Mid

Build working classes for users with a credit limit, purchases split into installments, repayments and dues, with clean interfaces.

SDE-3 / Senior

Handle edge cases (partial payments, overdue installments, blocking defaulters), money precision, and extensible installment plans (strategy pattern).

Staff / Principal

Map the design to a production service (ledger, idempotency, scheduled jobs for due dates) and discuss risk and credit decisions.


0) Problem Restatement

Flipkart's machine coding round: build a BNPL (Buy Now Pay Later) system from scratch in about 90 minutes, as working code, followed by a review round on your design. Typical requirements:

  • Onboard users with a credit limit (e.g., ₹5,000).
  • A user buys items using BNPL. The amount is taken from their available limit and split into installments (e.g., 3 monthly payments).
  • Users repay installments, which frees limit again.
  • Show dues: upcoming and overdue installments.
  • Block users who have overdue payments beyond N days, or who try to exceed their limit.
  • Report user status (limit, used, overdue).


1) Class Design

Architecture Diagram

classDiagram
    class User { +id +name +creditLimit +usedLimit +blocked }
    class Purchase { +id +userId +amount +createdOn +installments }
    class Installment { +purchaseId +number +amount +dueDate +paidAmount +status }
    class InstallmentPlan {
        <<interface>>
        +split(amount, startDate) List
    }
    class EqualMonthlyPlan
    class BNPLService {
        +onboard(name, limit) User
        +buy(userId, amount, plan, today) Purchase
        +repay(userId, purchaseId, amount, today) void
        +dues(userId, today) List
        +refreshStatus(userId, today) void
    }
    InstallmentPlan <|.. EqualMonthlyPlan
    BNPLService --> User
    BNPLService --> Purchase
    Purchase --> Installment
    BNPLService --> InstallmentPlan

2) Code (Python)

from dataclasses import dataclass, field
from datetime import date, timedelta
import itertools

@dataclass
class Installment:
    number: int; amount: int; due: date; paid: int = 0
    @property
    def remaining(self): return self.amount - self.paid

@dataclass
class Purchase:
    id: int; user_id: int; amount: int; installments: list = field(default_factory=list)

@dataclass
class User:
    id: int; name: str; limit: int; used: int = 0; blocked: bool = False

class EqualMonthlyPlan:
    def __init__(self, count=3, days=30): self.count, self.days = count, days
    def split(self, amount, start):
        base, extra = divmod(amount, self.count)        # integer paise; spread the remainder
        return [Installment(i + 1, base + (1 if i < extra else 0), start + timedelta(days=self.days * (i + 1)))
                for i in range(self.count)]

class BNPLService:
    OVERDUE_BLOCK_DAYS = 10
    def __init__(self):
        self.users, self.purchases, self._ids = {}, {}, itertools.count(1)

    def onboard(self, name, limit):
        u = User(next(self._ids), name, limit); self.users[u.id] = u; return u

    def buy(self, user_id, amount, plan, today):
        u = self.users[user_id]
        self.refresh_status(user_id, today)
        if u.blocked: raise PermissionError("user blocked due to overdue dues")
        if amount <= 0 or u.used + amount > u.limit: raise ValueError("insufficient credit limit")
        p = Purchase(next(self._ids), user_id, amount, plan.split(amount, today))
        u.used += amount; self.purchases[p.id] = p
        return p

    def repay(self, user_id, purchase_id, amount, today):
        p = self.purchases[purchase_id]
        if p.user_id != user_id: raise PermissionError("not your purchase")
        u = self.users[user_id]
        for inst in sorted(p.installments, key=lambda i: i.due):   # oldest dues first
            if amount == 0: break
            pay = min(amount, inst.remaining)
            inst.paid += pay; amount -= pay; u.used -= pay            # repaid money frees limit
        if amount > 0: raise ValueError("payment exceeds outstanding amount")
        self.refresh_status(user_id, today)

    def dues(self, user_id, today):
        return [(p.id, i.number, i.remaining, i.due, "OVERDUE" if i.due < today else "UPCOMING")
                for p in self.purchases.values() if p.user_id == user_id
                for i in p.installments if i.remaining > 0]

    def refresh_status(self, user_id, today):
        worst = max((today - d).days for _, _, _, d, s in self.dues(user_id, today) if s == "OVERDUE") 
                if any(s == "OVERDUE" for *_, s in self.dues(user_id, today)) else 0
        self.users[user_id].blocked = worst > self.OVERDUE_BLOCK_DAYS

3) Design Choices to Explain in Review

  • Money in integers (paise/cents), never floats. Split remainders are spread across the first installments, so the total always matches exactly.
  • Strategy pattern for installment plans: add a PayIn4Plan or InterestBearingPlan without changing the service.
  • Repayment order: oldest due first, and partial payments are allowed.
  • Limit accounting: used increases on purchase and decreases on repayment, so available = limit - used.
  • Blocking rule lives in one method (refresh_status), easy to change (e.g., 10 days overdue → blocked; paying everything unblocks).
  • Validation and errors: clear exceptions for over-limit, blocked users, overpayment and wrong owner.


4) Taking It to Production

  • Store users, purchases, installments and payments in a DB, and record money movements in an append-only ledger.
  • Idempotency keys on buy and repay, since payment callbacks retry.
  • A daily job marks installments overdue, sends reminders, applies late fees and blocks users. Don't compute everything on read.
  • Concurrency: update used with a conditional update (used + amount <= limit) so two simultaneous purchases can't exceed the limit.
  • Credit decisions: the limit comes from a risk model at onboarding and is adjusted by repayment behavior.


5) Wrap-Up

Model users (limit, used, blocked), purchases and their installments, with an installment-plan strategy that splits integer amounts exactly. Buying checks the block status and available limit, repaying pays the oldest dues first and frees limit, and a single status method blocks users with long-overdue installments. In production, add a ledger, idempotency, conditional limit updates and a daily overdue job.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →