/ / system design

HOW I DESIGN

Architectures I have designed, the decisions behind them, and what each decision costs.

Architecture design

Multi-tenant gym management SaaS

One platform serving many independent gyms, each with its own members, trainers and class schedule. Scheduling is the core feature; billing and reminders build on it.

  • Node.js
  • Express
  • MongoDB
  • Redis
  • React
ClientsAPIModulesDataExternalMember appPWAOwner dashboardper gymNode.js APImodular monolithAuth & tenantsSchedulingclasses · trainersMembershipsplans · paymentsNotificationsMongoDBtenantId on every documentRedis queuereminders · renewalsSMS / WhatsAppPayment gateway
  • Member app → Node.js API
  • Owner dashboard → Node.js API
  • Node.js API → Auth & tenants
  • Node.js API → Scheduling
  • Node.js API → Memberships
  • Node.js API → Notifications
  • Auth & tenants → MongoDB
  • Scheduling → MongoDB
  • Memberships → MongoDB
  • Notifications → Redis queue
  • Redis queue → SMS / WhatsApp
  • Memberships → Payment gateway

Scroll sideways to see the whole diagram.

Key decisions

  1. 01One shared database, tenantId on every document

    Why
    Hundreds of small gyms means cheap onboarding, one schema to migrate and one thing to back up.
    Trade-off
    Isolation depends on code. All queries go through a tenant-scoped repository and are covered by tests; a large chain can later move to its own database.
  2. 02Modular monolith, not microservices

    Why
    A small team ships faster with one deployable. Modules (scheduling, billing, notifications) have clear boundaries so they can be split out later.
    Trade-off
    Everything scales together. That is fine for this load, and cheaper to run until it is not.
  3. 03Scheduling first, with atomic bookings

    Why
    Class and trainer scheduling is the wedge feature. A booking decrements capacity in a single conditional update, so two members can never take the last spot.
    Trade-off
    Waitlist promotion adds logic (who gets a freed spot, and when). It is worth it: full classes are where gyms lose money.
  4. 04Background queue for reminders and renewals

    Why
    Reminders and subscription renewals must not slow down requests or be lost on a restart.
    Trade-off
    One more piece of infrastructure to run and monitor.
  5. 05Payments behind an interface

    Why
    Gyms in Tunisia often take cash or local payment methods. The billing module records payments first and talks to a gateway through an interface, so a local provider can be added without touching the rest.
    Trade-off
    Reconciling cash and online payments needs clear states per invoice.

Architecture design

Betting platform: sportsbook and casino

A design exercise for a platform with live sports odds and casino games. The hard parts are money correctness and live data, not the screens. Not a live product.

  • Node.js
  • PostgreSQL
  • Redis
  • WebSocket
  • React
ClientEdgeServicesCore & integrationsStorageWeb appbet slip · live oddsAPI gatewayREST + WebSocketSportsbookmarkets · settlementCasinogame sessionsOdds &results feedRedisodds cache · pub/subJob queuesettlementWallet & ledgerevery movementCasino gameproviderPostgreSQLdouble-entry ledger
  • Web app → API gateway
  • API gateway → Sportsbook
  • API gateway → Casino
  • Sportsbook → Odds & results feed
  • Sportsbook → Redis
  • Sportsbook → Job queue
  • Sportsbook → Wallet & ledger
  • Casino → Wallet & ledger
  • Casino → Casino game provider
  • Wallet & ledger → PostgreSQL

Scroll sideways to see the whole diagram.

Key decisions

  1. 01A double-entry ledger, not a balance column

    Why
    Every bet, win and refund is a pair of entries that always sums to zero. Any balance can be rebuilt and audited, and money cannot silently appear or vanish.
    Trade-off
    More tables and heavier queries than a single balance. PostgreSQL transactions make it safe, and a cached balance keeps reads fast.
  2. 02Idempotency keys on every money movement

    Why
    Networks retry. A repeated request with the same key returns the first result instead of charging twice.
    Trade-off
    Keys must be stored and expired, and every client has to send them.
  3. 03Odds from a feed, cached in Redis, pushed over WebSocket

    Why
    Live odds change every few seconds. Pushing beats polling, and the cache protects the feed and the database.
    Trade-off
    A bet slip can hold stale odds. Each odds value carries a version: if it changed, the bet is rejected with the new price instead of being accepted at the old one.
  4. 04Casino games from a provider, over signed webhooks

    Why
    A fair random engine needs certification. Integrating a provider means bet, win and rollback callbacks that the wallet handles idempotently.
    Trade-off
    Dependence on the provider and its fees. Webhook signatures are verified and replays are ignored.
  5. 05Settlement as asynchronous jobs

    Why
    When a match ends, thousands of bets settle at once. A queue spreads the load and lets a failed job retry without double-paying.
    Trade-off
    Winnings appear a few seconds after the result, not instantly.

Client work, anonymized

Document extraction pipeline

Turns uploaded contracts and invoices into clean, structured data that other checks can trust, whatever the language or the extraction engine behind it.

  • Node.js
  • Azure Document Intelligence
  • IPFS
  • LLM
ClientIngestPrepareExtractorsNormalizeResultClient appUpload APIvalidates · creates a jobFile storage+ IPFS pinningLanguagedetectionAzure DocumentIntelligenceLLM extractorcontractsNormalizerto document schemaResult storestatus · data
  • Client app → Upload API
  • Upload API → File storage
  • Upload API → Language detection
  • File storage → Azure Document Intelligence
  • Language detection → Azure Document Intelligence
  • Language detection → LLM extractor
  • Azure Document Intelligence → Normalizer
  • LLM extractor → Normalizer
  • Normalizer → Result store

Scroll sideways to see the whole diagram.

Key decisions

  1. 01Extractors behind one interface

    Why
    Azure handles invoices well; contracts benefit from an LLM. A common interface lets each document type use the best engine, and lets the client compare or replace them.
    Trade-off
    The interface can only expose what every engine provides, so engine-specific extras need a deliberate escape hatch.
  2. 02Normalize right after extraction

    Why
    Every engine returns a different shape. Mapping each one to the agreed document schema immediately means the rest of the system never sees a vendor format.
    Trade-off
    A mapping to write and maintain per engine and per document type.
  3. 03Keep the original, referenced by its hash

    Why
    Pinning the file on IPFS gives it a content address: anyone can prove the file behind a result was not changed afterwards.
    Trade-off
    IPFS content is public by default, so sensitive files must be encrypted before pinning. Pinning also has a cost.
  4. 04Detect the language before extracting

    Why
    Documents arrive in several languages. Knowing the language first lets the pipeline choose the right model settings.
    Trade-off
    A detection error cascades into extraction, so low-confidence results fall back to a multilingual setting.
  5. 05Asynchronous jobs with a status

    Why
    OCR takes seconds. The upload returns a job id right away and the client polls or is notified, so large files never block a request.
    Trade-off
    More states to model: queued, processing, done, failed.