Skip to content
← Selected work

2023 — presentSenior Software Engineer · Classera

Multi-Tenant Education & Commerce Platform

One codebase, many organisations, millions of users

users across tenant organisations
Millions
production stability
99.8%
data isolation and customisation
Per-tenant

The problem

A single platform had to serve many independent organisations — schools, districts and storefronts — each expecting its own data, branding and rules. Tenancy handled in application code is one forgotten WHERE clause away from a data leak, and a shared request path means one busy tenant degrades everyone else.

The approach

Multi-tenancy pushed into the schema rather than defended in the service layer, with PostgreSQL and TypeORM enforcing isolation, and an event-driven backbone that keeps slow work off the request path. Kafka carries high-throughput streams, RabbitMQ carries work that must not be lost, and Redis handles caching and pub/sub fan-out.

Architecture

  1. Angular (v10+) front ends with NgRx for global state and RxJS for reactive data flows, served across tenant-specific configurations.
  2. NestJS services behind a single ingress; every request resolves its tenant at the boundary, and nothing downstream reads tenant identity from user input.
  3. PostgreSQL with TypeORM as the system of record — isolation is a schema property, so no code path can bypass it.
  4. Apache Kafka for high-throughput event streaming between services; RabbitMQ for asynchronous work that needs delivery guarantees.
  5. Redis for read-through caching and pub/sub, backing real-time dashboards and notifications alongside WebSockets and Firebase Realtime Database.

Tenancy belongs in the schema

The tempting version enforces tenancy in a service-layer guard. That works until a batch job, a support script or a new endpoint writes to the same table without going through it. Making the database enforce isolation means the failure mode is a rejected query rather than a silent cross-tenant read — and it stays true for every code path written after I leave.

Kafka and RabbitMQ do different jobs

They are frequently treated as interchangeable. Kafka is a durable, replayable log: right for analytics streams and anything you may need to reprocess. RabbitMQ is a broker with per-message acknowledgement and dead-lettering: right for a single unit of work that must happen once and must not be lost. Using each for what it is good at removed a whole class of arguments about delivery semantics.

Real-time without a second architecture

Dashboards and notifications ride on Redis pub/sub and WebSockets rather than a separate real-time stack. Services publish the same domain events they already emit; the socket layer is a subscriber like any other. That keeps one source of truth for what happened, instead of a live path and a persisted path that drift apart.

Stability came from tests, not heroics

99.8% production stability is a consequence of Jest unit coverage and Cypress end-to-end suites running in GitLab CI on every merge, not of careful deploys. When the pipeline is the gate, releases get boring — which is the point.