Jun 11, 2024 · 3 min read · Production, Reliability, and Scale
Scaling a Multi-Tenant SaaS Product
A growing SaaS product didn’t immediately need more services. First, we needed to know which workload was growing and where the current system was reaching a limit.
While building a multi-tenant SaaS product as a freelancer, I owned work across the backend, database, deployment, and production support. As more workflows were added, “Will this scale?” started appearing in design conversations. It’s a fair question and, without a specific workload, a difficult one to answer. Should the application be separated into services? Did we need a different database? Should every slow operation move to a queue? Would a more complex cloud architecture prepare us for future growth? All of these decisions were possible, but none was useful until we understood what was actually growing.
What Was Actually Growing?
Different users create different pressure. One may read a small amount of data. Another may generate a report across months of activity. A background job may process more work than hundreds of ordinary requests. A single poorly bounded query can affect the database more than a large number of lightweight endpoints. We looked at the important access patterns rather than treating all traffic as equivalent. Some requests were fast and stateless. Some needed careful database indexes. A few longer operations were better handled in background jobs so they didn’t occupy the user-facing request path. The system didn’t have one general scaling problem. It had different workloads with different needs.
The Existing Architecture
The product used Node.js, TypeScript, NestJS, PostgreSQL, and AWS. A modular application gave us enough separation to improve specific areas without immediately distributing the entire system. We could increase application capacity when request volume required it, optimize the database where data access was the limit, and move appropriate work to background processing. This kept the operational model understandable while the product was still evolving. Separating everything into services early would have created more deployments, network communication, monitoring, and decisions about data ownership before the workload justified them. It might look more scalable in a diagram while making our small team slower.
Capacity and Efficiency
Scaling infrastructure increases how much work a system can perform. Improving efficiency reduces the work required for each operation. Both matter. If a query reads unnecessary data, adding database capacity delays the problem. If one background task has no concurrency limit, adding more workers can overload the next dependency. If the application stores large responses repeatedly, increasing memory doesn’t question why the responses are large. We tried to understand efficiency before increasing capacity. When additional capacity was appropriate, we could add it with a clearer idea of what improvement to expect. It’s reasonable to design for growth. The danger is paying today for every future architecture the product might need. We kept boundaries clear, avoided hiding business logic inside framework code, and made longer work separable from the request path. Those decisions preserved options without requiring us to build every option immediately. Early in a product, I value the ability to change the system when we have evidence more than an architecture built for every possible future.
Because I was responsible for both building and operating the product, every extra component felt concrete. A new service meant another deployment and another place I’d eventually troubleshoot. We did move some longer work to background jobs and increased capacity where the measurements supported it, but we kept most of the product in a modular application. The architecture still looked simple, and for the stage of the product that was useful. Most of our scaling conversations became much easier after we stopped talking about “more users” and started talking about the specific query, job or integration that was growing.