Monolith to Multi-Tenant SaaS in 4 Sprints — No Rewrite Required

Can you turn a brittle legacy app into a multi-tenant SaaS without rewriting it from scratch? We just did. In four sprints, our team relaunched a seven-year-old e-commerce monolith as a subscription-based platform powered by Angular 19 SSR, Node 20 + Fastify and Terraform Cloud.This post distills everything that worked, what blew up in our faces, and a copy-paste migration checklist. Grab a coffee: 8-minute read.
1 — Why Even Migrate? (Hint: Money & Velocity) ☕
Metric |
Before |
After |
Delta |
---|---|---|---|
Monthly Release Cadence |
1 / month |
12 / month |
× 12 |
Infra Cost / Tenant |
€165 |
€97 |
-41 % |
LCP 75th p (field) |
4.1 s |
1.9 s |
-54 % |
Net Promoter Score |
34 |
65 |
+31 |
ROI kicker: each 1-second LCP drop boosted funnel conversion by 6 %. Numbers made finance very, very happy.
2 — Audit the Monster in 3 Dimensions 🕵️
Before touching code, we ran a 3-D audit. Score every module 1 → 5:
Dimension |
5 = Red-Zone Symptoms |
---|---|
Coupling |
Cross-module imports, fat controllers, tangled AngularJS & jQuery |
Test Coverage |
< 10 % paths exercised |
Rollback Blast Radius |
DB migrations are irreversible, prod config differs from staging |
Rule of thumb: anything scoring ≥ 4 goes into the “strangler fig” backlog—decouple after you stabilize the happy path.
3 — Architecture Choice: Modular Monolith + Feature Flags 🚀
Why not micro-services right away?
Option |
⏱️ Speed to Ship |
🔒 Tenant Isolation |
👷♂️ Ops Burden |
---|---|---|---|
Lift-and-Shift Docker |
⚡ Fast |
😰 Minimal |
😀 Low |
Modular Monolith + Flags |
🔄 Balanced |
🙂 Good |
🟡 Medium |
Micro-services (DDD) |
🐢 Slow |
😎 Great |
🔴 High |
We chose Modular Monolith:
- Single repo keeps onboarding trivial.
- Feature flags let us ship dark features to one tenant at a time.
- Move to services only when a module outgrows the monolith.
4 — Frontend Overhaul: Angular 19 with Native SSR 🖼️
# add server-side rendering in two commands
npx ng add @angular/ssr
npm run build:ssr && npm run serve:ssr
Two lessons learned
- Lazy-hydrated Islands: heavy graphs & charts blew up
renderApplication
memory. We wrapped them withngSkipHydration
and hydrated onIntersectionObserver
. - TC39 Temporal API: Angular 19’s new date pipes + Node 20 eliminated 30 kB of Moment.js dead weight.
Result: LCP < 2 s on real Moto G4 devices.
5 — Backend & Tenancy: Fastify + Postgres RLS 🗄️
- Fastify because 80 k req/s on a single M6g large with zero tuning.
- Row-Level Security (
policy USING (tenant_id = current_setting('app.tenant_id'))
) keeps one DB until we hit 1 TB—then we partition. - Observability: OpenTelemetry → Grafana Cloud; one dashboard per tenant with UID templating.
6 — CI/CD: Green-Only Deploys in 45 Lines 📦
# .github/workflows/deploy.yml (core)
on: [push]
jobs:
test: … # npm ci && npm test
build_ssr: … # npm run build:ssr
deploy:
needs: build_ssr
runs-on: ubuntu-latest
permissions: { id-token: write }
steps:
- uses: hashicorp/setup-terraform@v3
- run: terraform init && terraform apply -auto-approve
Prod deploy in 11 min. If tests fail, prod is untouched.
7 — Security First (Really) 🔐
Layer |
Must-Have Control |
Tooling |
---|---|---|
Auth |
Passwordless magic-link + OAuth 2.1 |
Auth.js & Argon2 |
API |
Per-tenant rate-limit + HMAC sigs |
Fastify hooks, Redis |
Data |
AES-256 PII encryption + RLS |
Postgres 15, AWS KMS |
Infra |
CIS Level 1 as code |
tfsec, Open Policy Agent |
Fun fact: Week 1, 37 % of traffic was credential-stuffing bots—blocked automatically.
8 — Cost Lever Matrix 💸
Lever |
Year-1 Savings |
How |
---|---|---|
Edge Caching |
-23 % |
Cloudflare caches SSR HTML + stale-while-revalidate |
Serverless Cron |
-11 % |
Nightly reports moved to AWS Lambda |
Cloud Credits |
-17 % |
AWS Activate + open-source sponsorship |
Multi-AZ |
+6 % cost |
Worth it: SLA 99.95 % → churn -1.2 % |
9 — Five Lessons We Keep Re-Learning 🤹♂️
- Feature flags > long-lived branches.
- Measure field LCP, not just Lighthouse.
- Docs or die. Every interface change = one ADR file.
- Tenant-id on Day 0 – retro-fitting is hell.
- Post-launch “broken windows” sprint saves morale.
10 — Pocket Checklist (Steal Me) ✅
- ☐ Build a coupling matrix
- ☐ Add
tenant_id
column everywhere now - ☐ Ship risky slices behind flags
- ☐ Synthetic health check per tenant
- ☐ Schedule “Fix Broken Windows” sprint after go-live
Shipping a SaaS is never one-click magic. But with a modular plan, ruthless DevOps discipline and an obsession for user experience, you can turn a creaky monolith into a growth flywheel in under a month. Share your war stories below—let’s swap scars! 🚀