From Single-Tenant to Multi-Tenant: How Atlassian Rebuilt for Millions of Customers
There’s a ceiling every SaaS company hits, usually when it’s too late to plan around it. You’ve built a product people love, your customer count is growing, and suddenly the architecture you shipped in year one becomes the single biggest risk to your business. Deployments take a day. A failed compute node takes a customer down with it. Your cloud bill keeps climbing even though half your fleet is idle.
Atlassian hit that ceiling with Jira and Confluence. The way they climbed over it — without rewriting either product — is one of the cleaner examples of pragmatic large-scale architecture I’ve seen. Having spent over 15 years in .NET and cloud systems, including several migrations I’d rather not repeat, the decisions they made here resonate deeply.
The Single-Tenant Trap
In Atlassian’s original cloud model, each compute node was pinned to a specific tenant. One node, one customer. It felt safe — isolation by design, no noisy neighbor problems. But the tradeoff was brutal in practice.
Fault isolation became fault propagation. When a compute node went down, exactly one customer lost their service. Multiply that across thousands of tenants and you have a steady drip of outages, each one small in blast radius but enormous in customer impact. There’s no graceful degradation here — a node failure is a customer failure.
Deployments were a 24-hour slog. Rolling out a change meant touching every node, one tenant at a time. Engineering velocity grinds down when your release process is a 24-hour batch job. Continuous delivery becomes a fantasy.
Resource utilization was terrible. Compute was provisioned per tenant for peak load, which meant most nodes were sitting idle most of the time. If your enterprise customer’s team finishes work at 6pm, that node burns money until morning. At scale, this inefficiency is measured in millions.
This is the single-tenant trap: you trade operational simplicity for a compounding debt in cost, reliability, and speed. Atlassian needed a way out.
The Multi-Tenant Shift: What Actually Changed
The core architectural move was conceptually simple: make compute nodes stateless and tenant-agnostic. Any node can serve any tenant’s request.
But “stateless compute” is easy to say and hard to do when your product has years of tenancy assumptions baked into the codebase. Jira and Confluence were not written with multi-tenancy in mind. Refactoring them from the inside would have taken years and introduced enormous regression risk.
Atlassian’s answer was to not touch the products at all.
Instead, they introduced an abstraction layer — a routing middleware that sits between incoming requests and the application layer. This middleware handles the tenant resolution problem so that the application code never has to think about it. The product sees a database connection. How that connection was resolved — which tenant, which shard, which region — is invisible to Jira and Confluence entirely.
This is the engineering insight worth stealing: you can retrofit multi-tenancy onto legacy code by adding a routing layer, not by rewriting the application.
The Tenant Context Service: Where the Real Work Happens
The piece that makes this work is the Tenant Context Service (TCS).
flowchart LR
A[User Request] --> B[Compute Node\nstateless]
B --> C[Tenant Context Service\nTCS]
C --> D{Cache Hit?}
D -- Yes --> E[Return Tenant Config]
D -- No --> F[DynamoDB\nWrite Source of Truth]
F --> E
E --> G[Tenant Database\nper-tenant isolation]
G --> H[Response]Every incoming request carries a tenant identifier — typically embedded in the URL or auth token. The compute node intercepts this and makes a call to TCS before doing anything else. TCS’s job is straightforward on the surface: resolve the tenant ID to a database connection string and configuration. Under the hood, it’s doing considerably more.
TCS handles 30,000+ requests per second. At that throughput, it cannot go to a database on every call. The service maintains an in-memory cache of tenant configurations across a regional cluster of TCS nodes. Most requests hit cache and return in microseconds. The compute node gets its database connection, serves the request, and the user never knows anything happened.
What’s preserved in this model is per-tenant database isolation. Each customer still has their own database. The multi-tenancy is at the compute layer — shared, stateless nodes — not the data layer. This keeps compliance and data isolation guarantees intact while solving the resource and reliability problems.
CQRS in Practice: DynamoDB, Kinesis, and SNS
The consistency story is where the architecture gets interesting. TCS needs to serve reads at very low latency while ensuring writes are durable and consistent. The solution is a concrete implementation of CQRS — Command Query Responsibility Segregation — though the implementation is more instructive than the pattern name.
Writes go to DynamoDB. When tenant configuration changes — a new connection string, a feature flag update, a plan change — that write goes directly to DynamoDB as the authoritative source of truth. DynamoDB provides strong consistency for writes. Nothing is committed until DynamoDB confirms it.
Reads come from regional TCS clusters. Instead of querying DynamoDB on every read (expensive, high latency), each regional TCS cluster maintains its own local cache of tenant configurations. Reads are served from memory, which is why TCS can handle 30,000+ req/sec without breaking a sweat.
The sync layer is Kinesis Streams. When DynamoDB receives a write, a stream event is emitted to AWS Kinesis Streams. Regional TCS clusters consume from these streams and update their local caches accordingly. The lag is measured in milliseconds — eventual consistency, but fast enough that it’s invisible to users in practice.
Cache invalidation uses SNS. When a configuration change is critical enough that you cannot wait even milliseconds for stream propagation, Atlassian uses AWS SNS to broadcast an invalidation event directly to all TCS nodes simultaneously. Each node drops the stale cache entry and will fetch fresh from DynamoDB on the next request.
This is CQRS not as an academic pattern but as a concrete infrastructure decision: separate the write path (DynamoDB, strong consistency) from the read path (regional cache, low latency), and connect them with an event-driven sync mechanism.
The “Aha” Moment: They Never Rewrote Jira
I want to dwell on this because it runs counter to the instinct many engineering teams have when faced with a legacy scaling problem.
The temptation is to say: “We need to modernize. We’ll do a full rewrite with multi-tenancy built in from day one.” That rewrite will take two years, ship six months late, and have regressions your QA team won’t find until a customer does.
Atlassian’s approach was more disciplined. They asked: what is the minimum we need to change to solve the routing and isolation problem? The answer was: add a middleware layer that resolves tenant context before the application code runs. Jira and Confluence received no changes to support multi-tenancy. The abstraction absorbed the complexity.
This is a pattern I’ve applied in .NET migrations — adding a request pipeline middleware that injects resolved context into the DI container, so the application layer never sees the multi-tenancy machinery. The application asks for IDbConnectionFactory and gets back a connection to the right tenant database. How that factory was configured is not its concern.
Patterns Any Team Can Steal
If you’re navigating a similar shift — or designing a SaaS system from scratch — here’s what Atlassian’s architecture offers:
Stateless compute is the foundation. If your compute nodes carry per-tenant state, you’ve coupled reliability to individual machines. Make nodes interchangeable and you gain both fault tolerance and horizontal scale.
Separate the routing problem from the application problem. A dedicated tenant resolution service (call it TCS or something else) lets you evolve routing logic independently from product logic. It also makes the routing layer testable and observable in isolation.
CQRS for high-read, high-consistency systems. Strong-consistency writes to a durable store (DynamoDB), fast reads from a regional cache, event-driven sync between them. This pattern scales well and the pieces are all available as managed services on AWS.
Preserve data isolation even when sharing compute. Per-tenant databases satisfy compliance requirements and contain blast radius when something goes wrong at the data layer. Multi-tenancy at the compute layer is an efficiency play; data isolation is a trust play. You need both.
Cache invalidation via broadcast. SNS fan-out to all nodes is a simple, reliable mechanism for ensuring configuration changes propagate immediately when you need them to. It’s not elegant, but it works at scale.
Closing: Scale Is About State, Not Servers
Atlassian’s migration from single-tenant to multi-tenant cloud is a lesson in where the real difficulty of scale lives. It’s not in adding servers. AWS will sell you as many servers as you want.
The difficulty is in how you manage state, data, and tenants as your system grows. Which layer owns consistency? How do you route a request to the right context without coupling that knowledge into every layer of your stack? How do you make compute cheap and abundant without sacrificing the isolation your customers depend on?
Atlassian’s answer — stateless compute, a dedicated tenant routing service, CQRS across a managed event pipeline, and an abstraction layer that kept legacy products unchanged — is a playbook worth studying. Not because you should copy it exactly, but because the thinking behind each decision is transferable to almost any SaaS scaling problem you’ll face.
Scale is not about adding servers. It’s about knowing where your state lives, who owns it, and how you get it to the right place at the right time.