What I Learned About Scaling to One Million Users While Building My Placement Portal
Yesterday, I was reading the first chapter of System Design Interview Volume 1 by Alex Xu.
The chapter walks through how applications can evolve from a single server into systems capable of supporting millions of users.
What interested me most wasn't the list of technologies.
It was the way the architecture changes as the requirements change.
While reading, I started mapping those ideas to my own Placement Portal.
My project is nowhere near the scale discussed in the book.
And that's exactly why the exercise was interesting.
Instead of asking:
"How do I build a system for one million users?"
I started asking:
"What architectural problems will appear if this system actually grows?"
That changed the way I think about several decisions I'm making today.
1. Horizontal Scaling Is Usually More Flexible
One of the first concepts that stood out to me was horizontal scaling.
Instead of continuously upgrading a single machine, a system can add more application instances and distribute traffic between them.
For example:
Load Balancer
│
┌─────────┼─────────┐
▼ ▼ ▼
Django Django Django
Instance Instance Instance
This allows capacity to be increased by adding additional instances.
But that doesn't mean horizontal scaling is always the correct answer.
For a small application, vertical scaling can be simpler and more economical.
The appropriate strategy depends on things such as:
- Traffic patterns.
- Infrastructure costs.
- Application requirements.
- Operational complexity.
- Expected growth.
That's one of the biggest lessons I'm taking away from system design:
Scalability is a trade-off, not a checkbox.
2. Stateless Web Applications Make Scaling Easier
Another concept that immediately reminded me of my Placement Portal was the idea of a stateless web tier.
If multiple application instances are serving requests, each instance should ideally be capable of handling any request.
That becomes much harder if important user state is tied to one particular application server.
A stateless architecture makes the system easier to scale because requests don't have to keep returning to the same server.
This also connected with the JWT-based authentication approach I implemented in my Placement Portal.
The important lesson for me wasn't simply:
"Use JWT."
It was understanding why stateless application servers make horizontal scaling easier.
3. Databases Can Become the Bottleneck
Adding more application servers doesn't automatically solve every scalability problem.
Eventually, the database can become a bottleneck.
One common strategy for read-heavy applications is database replication.
A simplified architecture might look like:
Application
│
┌───────┴───────┐
▼ ▼
Primary Read Replica
Database Database
The primary database handles writes while read replicas can handle some read workloads.
This can improve scalability and availability when the workload justifies it.
My Placement Portal doesn't need this architecture today.
But understanding it changes how I think about database design.
The important question isn't:
"Can PostgreSQL handle my application?"
It's:
"What happens to my database when the application's workload changes?"
4. Background Tasks Shouldn't Block Requests
This was probably the easiest concept for me to relate to because I've already encountered the problem in my own project.
Some operations simply don't belong inside the request-response cycle.
For example:
- Sending emails.
- Processing resumes.
- Generating reports.
- Running scheduled operations.
- Performing other potentially expensive background work.
Instead of forcing the user to wait for those operations, they can be delegated to background workers.
That's why I've introduced Celery and RabbitMQ into my Placement Portal architecture.
The basic flow becomes:
Django
│
▼
RabbitMQ
│
▼
Celery Worker
│
▼
Background Task
Reading about message brokers in system design gave me another perspective on the architecture I was already building.
I wasn't simply adding Celery because it was a popular Django tool.
I was learning the architectural reason for separating task creation from task execution.
5. Caching Can Reduce Expensive Work
Another concept that stood out to me was caching.
If the same data is requested repeatedly, it doesn't always make sense to perform the same expensive operation every time.
A cache can store frequently accessed information and reduce pressure on the underlying systems.
In my Placement Portal, possible candidates for caching could include:
- Frequently accessed configuration.
- University information.
- Dashboard data.
- Placement statistics.
I have also been considering Redis as part of the project's future infrastructure.
But again, there's an important distinction:
Understanding caching doesn't mean everything should be cached.
Caching introduces its own problems around:
- Cache invalidation.
- Stale data.
- Memory usage.
- Consistency.
So the lesson isn't simply "cache everything."
It's:
Cache the things where the performance benefit justifies the additional complexity.
6. Multiple Data Centers Change the Failure Model
One of the more interesting concepts I encountered was deploying infrastructure across multiple data centers.
At first, it sounds like pure performance optimization.
But the bigger benefit is resilience.
If an entire infrastructure location becomes unavailable, having another location can help keep the system operational or support recovery.
This introduces concepts such as:
- Fault tolerance.
- High availability.
- Disaster recovery.
- Geographic redundancy.
Obviously, deploying a multi-data-center architecture for my current Placement Portal would be excessive.
But understanding the concept helps me think beyond:
"What happens when the server crashes?"
and toward:
"What happens when an entire infrastructure environment becomes unavailable?"
That's a very different level of system thinking.
7. Database Sharding Is a Different Level of Scale
Another concept discussed in large-scale system design is database sharding.
Instead of putting all data into one database, data can be distributed across multiple database instances.
For extremely large systems, this can help distribute:
- Storage.
- Read/write workloads.
- Database traffic.
But sharding also introduces significant complexity.
Applications need to understand how data is distributed.
Queries can become more complicated.
Transactions and consistency can become harder to manage.
My Placement Portal obviously doesn't need sharding today.
But learning about it helped me understand something important:
Scaling isn't free.
Every scaling strategy solves certain problems while introducing new ones.
What Would My Placement Portal Look Like at Scale?
This was the most interesting exercise I did while reading.
If I had to design a cloud architecture for the Placement Portal with significantly larger traffic, I might start thinking in terms of multiple environments:
- Production.
- Pre-production/testing.
- Disaster recovery.
A production architecture could potentially contain:
Load Balancer
│
┌──────────┴──────────┐
▼ ▼
Django Instance Django Instance
│ │
└──────────┬──────────┘
│
┌──────────┴──────────┐
▼ ▼
Primary DB Read Replicas
│
│
┌─────┴─────┐
▼ ▼
Redis RabbitMQ
│
▼
Celery Workers
I could then add supporting infrastructure for the frontend, monitoring, scheduled tasks, and other workloads as the requirements justified them.
A possible starting configuration might include:
- 2 Django application instances.
- 2 Next.js application instances.
- 1 PostgreSQL primary.
- 2 PostgreSQL read replicas.
- Redis for caching.
- RabbitMQ for background task messaging.
- Celery workers for background processing.
But I want to emphasize something:
This is a thought exercise, not my current production architecture.
My Placement Portal is still being developed locally.
The value of this exercise isn't predicting the exact number of servers I'll eventually need.
It's understanding what kinds of architectural problems will appear as the system grows.
The Biggest Constraint Isn't Technology
This may actually be the most important lesson.
When looking at large architecture diagrams, it's easy to get excited about adding:
- More servers.
- More replicas.
- More databases.
- More queues.
- More caches.
- More regions.
But infrastructure costs money.
And infrastructure introduces operational complexity.
So the real question isn't:
"What is the most scalable architecture I can design?"
It's:
"What is the simplest architecture that satisfies the current requirements while leaving a reasonable path for growth?"
That's a much harder question.
And it's also much closer to real engineering.
Don't Design for One Million Users on Day One
If I try to build my Placement Portal for one million users before I have even acquired my first real users, I would probably spend more time solving imaginary problems than real ones.
Today, I don't need database sharding.
I don't need multiple data centers.
I don't need dozens of application instances.
I don't need an enormous distributed architecture.
What I do need is an architecture that doesn't make future growth unnecessarily painful.
That's a much more practical goal.
Build for today's requirements.
Understand tomorrow's problems.
Avoid decisions that unnecessarily prevent tomorrow's architecture.
Final Thoughts
Reading about systems designed for millions of users taught me something I didn't expect.
System design isn't really about one million users.
It's about understanding how a system evolves.
A small application might start with:
Application
│
▼
Database
Then requirements appear.
Background jobs are introduced.
Caching becomes useful.
Read traffic increases.
Workers need to scale.
Databases need replication.
Eventually, reliability requirements become more demanding.
The architecture evolves with them.
That's the mindset I'm trying to develop while building my Placement Portal.
I don't want to build a million-user architecture today.
I want to understand what a million-user architecture looks like so that I can recognize the problems when they eventually become real.
System design isn't about preparing for success by over-engineering everything.
It's about making sure success doesn't become the reason your system fails.
— Abhishek Roka