Insight — May 28, 2026 · 7 min

820ms to 94ms: rebuilding a monolith into a layered backend

Astro Tales' backend had grown feature by feature until adding a notification provider meant touching six files. The Controller → Service → Provider rebuild cut P95 response times 8×, and made 50,000 daily push notifications land inside a 12-minute window.

BackendNode.jsArchitectureRedis

How backends rot

Astro Tales had a working React Native app and a user base growing 15–20% month over month. The backend, though, had been written incrementally with no defined architecture — and it showed. Adding a new notification provider required changes in six different files. A bug in the payment webhook handler once silently failed 200 subscription renewals before anyone noticed.

The scaling wall was the morning horoscope: 50,000+ users expect personalised content between 6 and 7 AM, before they leave for work. Direct API calls from the web server were failing 3–8% of the time at peak. That's not a tuning problem; it's an architecture problem.

Controller → Service → Provider

We rebuilt from scratch on a strict three-layer pattern. Controllers handle HTTP and input validation (Zod schemas) and nothing else. Services hold business logic with zero I/O dependencies — pure functions, unit-testable without a database. Providers are thin adapters over external services (Supabase, Razorpay, Twilio, Firebase, Expo Push), swappable behind interfaces.

That last layer is what killed the 'touch six files' problem: a new provider is one new file implementing an interface, and nothing upstream knows the difference. When a Compatibility Report feature was added three months post-launch, it needed exactly one new Service and one new Provider — no modifications to existing code.

The CPU-heavy astrological computations (planetary positions, house calculations, dasha periods) are deterministic given birth parameters, so we extracted them into a computation service backed by Redis caching. Repeat requests — common when returning users check multiple chart types — serve from cache in under 10ms instead of recomputing.

The notification burst runs as a Bull queue backed by Redis: node-cron enqueues the day's sends at 5:45 AM, parallel workers process 500 notifications a second, and failed sends retry three times with exponential backoff.

The numbers after

P95 response time went from 820ms to 94ms. Notification delivery success rose from 96–97% to 99.9%, with the full 50,000-user run completing in under 12 minutes instead of 45. Zero subscription renewals have failed since the webhook handler was rebuilt with idempotency keys.

None of this required exotic infrastructure — Postgres, Redis, and a queue. The gains came from boundaries: when logic, I/O, and orchestration each live in exactly one place, both performance work and feature work stop fighting the codebase.