Building a production platform, one problem at a time.
Three years inside one multi-tenant rebate platform. Each chapter is a problem I met, the decision I made, and what it measured out to.
Multi-Tenant Rebate Platform
A group-buying SaaS handling rebate and incentive management across tenants. I am the only Django engineer on it.
Each problem was harder than the last.
Many clients, one codebase, and a hard requirement that no tenant ever sees another tenant's data.
Schema-based tenancy on one PostgreSQL database, via django-tenants.
Isolation at the database level, without a database or deployment per client.
Every migration has to run across every schema. Migrations get heavier as tenants grow.
One deployment, one codebase, and leakage prevented structurally rather than by review.
Isolation you can see in the schema is easier to trust than isolation enforced in code.
Not more features. More maturity.
Each stage changed what the platform could safely be asked to do next.
Every stage, the problem it solved.
Trainee and employee records were tracked by hand.
Built the internal management system that replaced it — my first production Django codebase.
Amazon sales data arrived faster than anyone could process it.
Built the SP-API integration and crawlers on FastAPI, moving large volumes of e-commerce data asynchronously.
A live client platform nobody could safely change.
Standardized the entire backend as sole Django engineer, then built payments and incentives and took endpoints from ~60s to under 5s.
The team had no shared way to adopt AI tools.
Led the org-wide evaluation and set how the tooling gets used — every generated line reviewed.
Four layers, one system.
What three years actually taught me.
Four things made one endpoint slow. Fixing three of them changed nothing.
Performance work is rarely a single fix. Until every contributing cause is gone the number barely moves — which is why you measure after each change, not only at the end.
Isolation you can see in the schema is easier to trust than isolation enforced in code.
A tenant filter is one forgotten WHERE clause away from a leak. A separate schema is a structural guarantee, and the migration cost is a fair price for it.
A standard only survives if adopting it does not require stopping feature work.
I proposed refactoring incrementally instead of a rewrite. That is why it shipped, and why the client kept it without changes.
Infrastructure sizing is part of the design, not an afterthought.
Search returned correct results in 90 seconds. The code was right; the heap was too small. A working feature on undersized hardware is still a broken feature.
Idempotency is not a payments detail — it is what every retry in the system assumes.
Once you have handled a webhook that arrives twice, you start seeing the same problem in queues, crawlers and background jobs.
Backward compatibility buys more goodwill than the feature you are shipping.
Adding payments to a live platform meant nothing existing was allowed to change behaviour. The constraint shaped the design more than the requirements did.
Developer experience is a product feature that ships to your team.
Cutting CI from 45 minutes to 5 changed how often people pushed. No user saw it; every engineer felt it.
Reviewing generated code is the work. Generation was never the bottleneck.
AI tooling made me faster at typing and no faster at deciding. The judgement still has to come from somewhere, so I read every line.