I'm always excited to take on new projects and collaborate with innovative minds.

Phone

+2347012499717

Email

hello@kingsleyanusiem.com

Social Links

Web Development

Building Software That Doesn't Break at 10,000 Users (Part III)

Part III — The Database: Where Scalability Often Dies

"Your application may have thousands of users, but eventually every request has to ask the database for something."

Building Software That Doesn't Break at 10,000 Users (Part III)

In Part I, we established that scalable software begins with engineering decisions rather than expensive infrastructure.

In Part II, we moved one layer deeper and examined architecture: how systems are structured, how responsibilities are separated, and why the wrong architectural decisions can make future growth unnecessarily painful.

But architecture is only part of the story.

Eventually, almost every application reaches the same bottleneck.

The database.

You can optimize your frontend.

You can add more application servers.

You can introduce caching.

You can move workloads into queues.

You can increase CPU and memory.

But if every request eventually reaches a database that wasn't designed to handle the workload, the database becomes the ceiling for everything else.

And this is where many applications begin to struggle.

Not at 10,000 users.

Sometimes at 1,000.

Sometimes at 500.

Sometimes even earlier.

The problem is rarely that databases are inherently slow.

The problem is that developers often design databases for the amount of data they have today rather than the amount of data the business will eventually accumulate.

A table with 10,000 rows can hide a lot of mistakes.

A table with 10 million rows usually doesn't.


The Database Is Not Just Storage

One of the earliest misconceptions developers encounter is thinking of the database as simply a place where application data is stored.

It is much more than that.

A database is responsible for answering questions.

Very quickly.

Your application constantly asks questions such as:

  • Which users registered today?
  • What transactions belong to this customer?
  • What orders are still pending?
  • What payments failed?
  • Which products are currently in stock?
  • What was this user's balance before the transaction?
  • Which notifications have not been delivered?
  • What transactions occurred between two dates?
  • Which users haven't logged in for 30 days?

Every one of these questions becomes a database operation.

And as your application grows, the number of questions increases dramatically.

A system with 500 users might generate thousands of queries every hour.

A system with 10,000 users might generate millions.

The database doesn't care that your application is successful.

It simply receives more work.

This is why database design must be treated as part of application architecture rather than something developers configure after the application has already been built.


The Dangerous Illusion of Small Data

During development, almost every database feels fast.

You create a users table.

You insert 100 records.

You run:

SELECT * FROM users;

The result appears immediately.

You create a transactions table.

You insert a few hundred transactions.

You run a query filtering by user ID.

Again, almost instant.

Everything feels perfect.

Then production happens.

The users table grows to hundreds of thousands of records.

The transactions table grows into millions.

Audit logs accumulate.

Notifications accumulate.

Activity history accumulates.

Payments accumulate.

And suddenly queries that used to execute in milliseconds begin taking seconds.

The code didn't necessarily change.

The data did.

This is one of the most important concepts in scalable database engineering:

A query that is fast with small data is not necessarily a query that scales.

Performance must be considered relative to data growth.


Database Design Begins With the Schema

Before worrying about indexes or database servers, you need to get the schema right.

A schema is effectively the structural blueprint of your database.

It defines:

  • Tables
  • Columns
  • Relationships
  • Constraints
  • Data types
  • Keys
  • Indexes

A poorly designed schema creates problems that become increasingly expensive to fix.

Consider a simple transaction table.

You might start with:

transactions

id
user_id
amount
status
created_at

Looks simple.

But real systems quickly become more complicated.

What currency is the amount?

Can a transaction have multiple currencies?

What happens when a transaction is reversed?

Where is the provider reference stored?

What identifies the transaction externally?

Can two transactions have the same reference?

What happens when a payment provider retries the request?

What happens if the application crashes halfway through processing?

These aren't merely coding questions.

They are database design questions.

And the answers determine whether your system remains reliable when transaction volume increases.


Data Types Matter More Than Developers Think

Choosing a database column type isn't just about making the migration work.

It affects storage, comparison, indexing, and sometimes performance.

For example, storing monetary values as floating-point numbers can create precision problems.

Imagine:

0.1 + 0.2

Mathematically, we expect:

0.3

But floating-point representation can produce results that aren't exactly 0.3.

For financial systems, this matters.

Money should generally be represented using an appropriate exact numeric strategy, such as fixed-precision decimal values or integer minor units, depending on the system's requirements.

The same principle applies elsewhere.

Dates should use appropriate date/time types.

Boolean values should not be represented as arbitrary strings.

Large identifiers may require larger integer types.

JSON can be useful, but it shouldn't become an excuse to put an entire relational model inside one column.

Every data type is a design decision.


Normalization vs Convenience

Database normalization is another concept developers often encounter early and then forget.

The basic idea is simple:

Don't unnecessarily duplicate the same piece of information across many records.

Imagine an orders table containing:

order_id
customer_id
customer_name
customer_email
customer_phone
product_id
product_name
price

At first, this seems convenient.

But what happens when the customer's phone number changes?

Now multiple records may contain the old value.

What happens when the product name changes?

Now historical records may contain inconsistent information.

Relational databases provide a better approach.

Separate the concepts into appropriate tables and connect them through relationships.

For example:

users
products
orders
order_items

Now each entity has a clearer responsibility.

However, normalization should also be applied thoughtfully.

Sometimes controlled denormalization is useful for performance, reporting, or read-heavy systems.

The goal isn't to follow database theory blindly.

The goal is to model the business accurately while understanding the trade-offs.


Relationships Become Expensive at Scale

Relationships are one of the strengths of relational databases.

But relationships also introduce work.

Consider:

users
orders
order_items
products

A simple dashboard might need:

  • User information
  • Order information
  • Product information
  • Payment information
  • Delivery information

A developer may write a query joining all of these tables.

With a small dataset, it may work perfectly.

As the dataset grows, however, the database has significantly more work to perform.

This is why understanding joins is essential for backend developers.

You don't need to avoid joins.

You need to understand:

  • Which tables are being joined
  • Which columns are being compared
  • How many records are involved
  • Whether the relevant columns are indexed
  • What execution plan the database chooses

A join isn't automatically slow.

An uncontrolled join across massive datasets can be.


The Index: One of the Most Powerful Tools in Database Performance

If there is one database concept every backend developer should understand deeply, it is indexing.

Imagine a library containing one million books.

You ask:

"Find every book written by Kingsley."

Without an organized catalog, someone may have to inspect every book.

That's essentially what happens when the database performs a full table scan.

An index provides a structure that helps the database locate relevant records without examining everything.

For example:

CREATE INDEX idx_users_email
ON users(email);

Now a query such as:

SELECT *
FROM users
WHERE email = 'example@email.com';

can potentially locate the relevant record far more efficiently.

But indexes are not free.

Every index consumes storage.

Every insert may require index updates.

Every update to an indexed column can require additional work.

Too few indexes can make reads painfully slow.

Too many indexes can make writes unnecessarily expensive.

Good database engineering is about finding the right balance.


Index What You Query

A common mistake is creating indexes simply because a column "might be useful."

Indexes should be based on actual access patterns.

If your application frequently executes:

WHERE user_id = ?

then user_id may need an index.

If your application frequently executes:

WHERE status = ?
AND created_at > ?

a composite index may be appropriate.

The important word is frequently.

Database indexes should support real queries.

Don't create dozens of indexes without understanding why they exist.


Composite Indexes

Sometimes a query filters by multiple columns.

For example:

SELECT *
FROM transactions
WHERE user_id = ?
AND status = ?
ORDER BY created_at DESC;

An index involving multiple columns may be more useful than separate indexes on each individual column.

For example:

(user_id, status, created_at)

But composite indexes have ordering rules.

The order of columns matters.

An index isn't simply a bag containing several columns.

The database organizes the index according to its structure.

This is why experienced developers don't just add indexes randomly.

They examine the actual queries.


The N+1 Query Problem

One of the most common performance problems in application development is the N+1 query problem.

Imagine you want to display 100 orders.

You run one query:

SELECT * FROM orders;

Then your application loops through those 100 orders and fetches the customer for each one.

Instead of one query, you might end up with:

1 query for orders
+
100 queries for customers
=
101 queries

With 10 orders, this might go unnoticed.

With 10,000 records, it becomes disastrous.

Modern frameworks provide tools for avoiding this problem.

For example, Laravel's eager loading:

Order::with('user')->get();

can retrieve related records more efficiently than repeatedly querying the database inside a loop.

The lesson isn't specifically about Laravel.

The lesson is:

Don't let application loops silently turn one database operation into hundreds or thousands.


Stop Using SELECT *

Another small habit that becomes expensive at scale is:

SELECT *

It is convenient.

It is also often unnecessary.

Suppose your table contains:

id
name
email
phone
address
profile_photo
biography
metadata
created_at
updated_at

But your API only needs:

id
name
email

Why retrieve everything?

Instead:

SELECT id, name, email
FROM users;

This reduces unnecessary data transfer and can reduce memory usage.

It becomes particularly important when tables contain large text fields, JSON data, or other expensive columns.

Fetch what you need.

Not everything that exists.


Pagination Is Not Optional at Scale

Imagine an API endpoint returning all transactions.

During development:

500 transactions

No problem.

After a year:

5,000,000 transactions

Now imagine returning all five million records in a single response.

Even if the database can execute the query, the application server has to process the data.

The network has to transfer it.

The client has to receive it.

The client may have to render it.

This is why pagination matters.

Instead of:

GET /transactions

returning everything, you might support:

GET /transactions?page=1&per_page=50

Modern systems may also use cursor-based pagination.

For very large datasets, cursor pagination can be more efficient than repeatedly calculating large offsets.

The important principle is simple:

Never make the database and API return millions of records simply because the client asked for "everything."


Offset Pagination Has a Hidden Cost

Consider:

SELECT *
FROM transactions
ORDER BY id
LIMIT 50 OFFSET 500000;

The database may still need to work through a large number of rows before reaching the requested offset.

For large datasets, cursor-based approaches can be more efficient.

For example:

GET /transactions?after=982734

The application can then retrieve records after a known position.

This approach becomes especially useful for feeds, transaction histories, logs, and other continuously growing datasets.


Transactions Protect Data Integrity

Scalability isn't only about speed.

It is also about correctness.

Imagine transferring money between two accounts.

You might need to:

  1. Subtract money from Account A.
  2. Add money to Account B.
  3. Create a transaction record.
  4. Update the transaction status.

What happens if step 1 succeeds but step 2 fails?

Now the system is inconsistent.

Database transactions exist to help handle operations that must succeed or fail together.

Conceptually:

BEGIN

Debit Account A

Credit Account B

Create Transaction Record

COMMIT

If something goes wrong:

ROLLBACK

The exact implementation depends on the database and application architecture, but the principle is universal.

A scalable system must not simply process more transactions.

It must process them correctly.


Concurrency Changes Everything

Two requests can arrive at almost exactly the same time.

Imagine a wallet contains:

₦100,000

Two withdrawal requests arrive simultaneously.

Request A wants:

₦80,000

Request B wants:

₦50,000

If both requests read the balance before either updates it, both may believe enough money exists.

This is a concurrency problem.

The database must help the application coordinate these operations safely.

Depending on the situation, solutions may involve:

  • Transactions
  • Row locking
  • Atomic updates
  • Isolation levels
  • Unique constraints
  • Idempotency

These concepts become increasingly important in financial, inventory, booking, and other systems where two users can attempt to modify the same resource simultaneously.


Idempotency: The Database's Quiet Superpower

Consider a payment request.

The client sends:

POST /payments

The server processes the payment.

Then the network times out.

The client doesn't know whether the payment succeeded.

So it retries.

Now the same payment request arrives twice.

If your system blindly creates a transaction every time, you could charge the customer twice.

This is why idempotency matters.

A client can provide an idempotency key:

payment_8f72a91

The server records it.

If the same request arrives again, the application recognizes that the operation has already been processed.

This can be reinforced at the database level with an appropriate unique constraint.

The result is a much safer system.

This principle applies far beyond payments.

It is useful for:

  • Orders
  • Withdrawals
  • Transfers
  • Webhooks
  • Notifications
  • Subscription creation
  • External API requests

At scale, retries are inevitable.

Your system must be designed for them.


Database Constraints Are Your Last Line of Defense

Application validation is important.

But don't rely on application code alone to protect your data.

Suppose an email must be unique.

You could check:

Does this email already exist?

Then insert the new user.

But two requests could arrive simultaneously.

Both might check.

Both might see that the email doesn't exist.

Both might attempt the insert.

A database-level unique constraint provides stronger protection:

UNIQUE(email)

The same idea applies to:

  • Unique transaction references
  • Unique usernames
  • Unique order numbers
  • Foreign key relationships
  • Required fields
  • Valid ranges

Your application should validate.

Your database should enforce critical invariants.


Foreign Keys Are Not the Enemy

Some developers avoid foreign keys because they believe they reduce performance or make databases harder to work with.

In many systems, foreign keys provide valuable protection against invalid relationships.

For example:

An order belongs to a user.

A payment belongs to an order.

An order item belongs to an order.

These relationships matter.

Without appropriate constraints, your database can accumulate orphaned records and inconsistent data.

Again, context matters.

Large-scale systems sometimes make deliberate trade-offs around database constraints, especially in distributed architectures.

But those decisions should be intentional.

Not simply because "constraints are annoying."


The Query Is Only Half the Story

When a query becomes slow, developers often immediately rewrite the SQL.

Sometimes that's necessary.

But first ask:

What is the database actually doing?

Modern relational databases provide query analysis tools.

For example:

EXPLAIN
SELECT *
FROM transactions
WHERE user_id = 123
ORDER BY created_at DESC;

The execution plan can reveal whether the database is:

  • Using an index
  • Scanning the entire table
  • Performing expensive joins
  • Sorting large datasets
  • Reading more rows than necessary

This changes optimization from guesswork into engineering.

Instead of saying:

"This query feels slow."

You can investigate:

"The database is scanning 4.8 million rows because the filtering column isn't indexed."

That's a much better problem to solve.


Caching Doesn't Fix Bad Queries

Caching is powerful.

But it can also hide problems.

Imagine a dashboard takes five seconds to load because it performs an expensive query.

You add Redis.

Now the dashboard loads in 50 milliseconds.

Problem solved?

Maybe temporarily.

But eventually:

  • Cache entries expire.
  • Data changes.
  • Cache invalidation becomes complicated.
  • More users generate more uncached requests.
  • Memory requirements increase.

Caching is useful when applied to the right problem.

It should not become a substitute for fixing an inefficient database query.

A useful order of thinking is:

1. Is the query correct?
2. Is the schema appropriate?
3. Are the required indexes present?
4. Is the query retrieving unnecessary data?
5. Is the access pattern efficient?
6. Only then: should this result be cached?

Database Caching and Application Caching Are Different

It's also important to distinguish between different layers of caching.

You might cache:

  • Database query results
  • API responses
  • User sessions
  • Configuration
  • Frequently accessed objects
  • Computed statistics

Each has different invalidation requirements.

For example, caching a list of blog posts is relatively simple.

Caching an account balance is much more sensitive.

The more important the data, the more carefully caching must be designed.

Fast incorrect data is still incorrect data.


Read-Heavy vs Write-Heavy Systems

Not every application stresses the database in the same way.

Some systems are read-heavy.

For example:

  • News platforms
  • Product catalogs
  • Public websites
  • Documentation systems

Others are write-heavy:

  • Financial platforms
  • Analytics systems
  • Logging platforms
  • Messaging systems
  • High-volume event processing

The architecture needs to account for the workload.

A database optimized for one workload may require different strategies for another.

This is why statements like:

"This is the best database architecture."

should immediately make you suspicious.

The better question is:

"Best for what workload?"


Read Replicas

As read traffic increases, one database server may eventually become a bottleneck.

One strategy is replication.

A primary database handles writes.

One or more replicas handle reads.

Conceptually:

                Application
                     |
            -------------------
            |                 |
          Writes             Reads
            |                 |
         Primary          Read Replica

This can significantly increase read capacity.

But replication introduces an important concept:

Replication Lag

The replica may not immediately contain the latest data written to the primary.

Imagine a user creates an order.

The application writes:

Order #1001

to the primary database.

Immediately afterward, another request reads from a replica.

If replication hasn't caught up, the application may temporarily behave as though the order doesn't exist.

This is called eventual consistency.

Again, the architecture isn't "better" simply because replicas exist.

It introduces trade-offs.


Database Partitioning and Sharding

Eventually, some systems become too large for a single database structure to handle efficiently.

At that point, more advanced techniques may become relevant.

Partitioning can divide a large table into smaller logical pieces.

For example, transactions could potentially be partitioned by:

Year
Month
Region
Tenant

Instead of one enormous logical dataset, the database works with smaller partitions.

Sharding goes further.

Data is distributed across multiple database instances.

For example:

Shard 1 → Users 1–1,000,000
Shard 2 → Users 1,000,001–2,000,000
Shard 3 → Users 2,000,001–3,000,000

But sharding introduces significant complexity.

Queries become more difficult.

Cross-shard transactions become difficult.

Data distribution becomes a major concern.

Operational complexity increases.

This is why sharding should not be the first answer to a slow query.


Don't Scale the Database Before Understanding the Problem

A common response to database performance problems is:

"Let's upgrade the server."

More CPU.

More RAM.

Faster storage.

A larger cloud instance.

Sometimes that works.

But it may only delay the problem.

If the real issue is:

Missing index

then more RAM doesn't fix the underlying query.

If the problem is:

N+1 queries

then a bigger server simply processes inefficient requests with more resources.

If the problem is:

SELECT *

across millions of rows, more CPU doesn't fundamentally change the access pattern.

Infrastructure can buy you time.

Engineering fixes the underlying problem.


Database Observability

You cannot reliably scale something you cannot observe.

Production databases should be monitored.

Important metrics can include:

  • Query latency
  • Slow queries
  • CPU usage
  • Memory usage
  • Connection count
  • Lock contention
  • Disk usage
  • Replication lag
  • Cache hit rates
  • Transaction throughput
  • Error rates

Logging also matters.

If an endpoint suddenly becomes slow, you should be able to determine whether the problem is:

Application
        ↓
API
        ↓
Database query
        ↓
Lock
        ↓
Disk

Without observability, developers end up debugging production systems by intuition.

That is not scalable.


Connection Pools Matter Too

Your application doesn't usually create a completely new database connection for every request.

Instead, production systems commonly use connection pooling.

This allows connections to be reused.

But connection pools have limits.

Imagine an application suddenly receives thousands of concurrent requests.

If every request attempts to create its own database connection, the database can become overwhelmed.

You can end up with errors such as:

Too many connections

This is another reason scalability isn't simply about adding more servers.

Ten application servers can actually make a database problem worse if each server opens too many connections.

Scaling one layer can overload another.

The entire system must be considered.


Data Growth Is a Product Problem

One of the most overlooked aspects of database scalability is data retention.

Ask yourself:

Does the application need to keep every record forever?

Logs are a good example.

A system may generate:

API logs
Authentication logs
Audit logs
Error logs
Webhook logs
Analytics events

If nothing is ever archived or deleted, the database grows indefinitely.

Eventually, even simple operations become more expensive.

A mature system should have data lifecycle policies.

For example:

Hot data
↓
Frequently accessed

Warm data
↓
Occasionally accessed

Cold data
↓
Archived

Expired data
↓
Deleted

Not every piece of data needs to remain in the primary production database forever.


Soft Deletes Have a Cost

Soft deletes are useful.

Instead of deleting a record:

DELETE FROM users

you might mark it:

deleted_at = timestamp

This allows recovery and historical tracking.

But there is a hidden cost.

The database still contains the record.

As the dataset grows, queries may repeatedly need to exclude deleted records.

Large systems therefore need to think carefully about retention, indexing, archiving, and whether every dataset actually needs soft deletion.

A feature that is convenient at 1,000 records can become expensive at 100 million.


The Database Should Reflect the Business

This is where database engineering connects back to Part II.

We discussed designing software around business domains.

The same principle applies to data.

A payments system should understand payments.

An inventory system should understand inventory.

An identity system should understand identity.

The database schema should represent meaningful business entities and relationships rather than simply reflecting whatever objects happened to be easiest to code.

When the business changes, the data model may need to evolve.

That evolution should be intentional.


Migrations Are Part of Production Engineering

Changing a production database is not the same as changing a local database.

Imagine adding a new column:

ALTER TABLE transactions
ADD COLUMN reference VARCHAR(255);

With a tiny database, the change may be almost invisible.

With hundreds of millions of records, schema changes can become operationally significant.

Depending on the database and exact operation, migrations may involve:

  • Locks
  • Long-running operations
  • Increased CPU usage
  • Increased disk usage
  • Replication delays
  • Temporary performance degradation

This is why experienced teams think carefully about database migrations.

Sometimes a safer approach is:

1. Add new column
2. Deploy code that supports both formats
3. Backfill existing records gradually
4. Switch application reads
5. Switch application writes
6. Remove old column later

This is more complicated than changing a migration locally.

But production systems require production thinking.


Zero-Downtime Changes

One of the most dangerous assumptions is:

"We can just deploy the database change and then deploy the application."

What happens if old application servers are still running?

During a rolling deployment, you may temporarily have:

Old Application
+
New Application
+
Old Schema
+
New Schema

The old code must still work while the new code is being introduced.

This is why backward-compatible database changes are so important.

Good deployments are designed around transition states.

Not just the final state.


Your Database Is Part of Your API

Developers often think of APIs as HTTP endpoints.

But the database schema also becomes a kind of internal contract.

Once application logic depends heavily on a particular structure, changing that structure becomes expensive.

This is another reason abstraction matters.

Your business logic shouldn't be scattered everywhere with assumptions about raw database implementation details.

Good boundaries make future migrations easier.

Maybe one day you change:

MySQL

to another relational database.

Or introduce:

Read replicas

Or move analytics into:

A dedicated analytics platform

The cleaner your boundaries, the easier those transitions become.


Don't Put Everything in the Database

The opposite mistake also exists.

Some systems attempt to use the database for everything.

Images.

Large files.

Temporary sessions.

Caches.

Search.

Analytics.

Logs.

Background jobs.

The database becomes the center of the universe.

That can work at small scale.

But specialized systems exist for specialized workloads.

You might eventually use:

Object Storage → Files
Redis → Caching
Queue → Background Jobs
Search Engine → Full-text Search
Analytics Warehouse → Large-scale Analytics
Relational Database → Transactional Data

The goal isn't to introduce all these technologies immediately.

The goal is to recognize when a workload has outgrown the tool currently handling it.


The Most Important Database Optimization Is Often Simplicity

Developers sometimes assume scalable database architecture requires complicated technology.

Not necessarily.

A well-designed relational database with:

  • Good schema design
  • Appropriate indexes
  • Efficient queries
  • Proper constraints
  • Pagination
  • Transactions
  • Monitoring
  • Sensible data retention

can support significant workloads.

You don't need five database technologies because your startup has 300 users.

You need to understand the workload.

Start with the simplest architecture that solves today's problems while preserving a reasonable path toward tomorrow's requirements.


A Practical Database Scalability Checklist

Before declaring a database production-ready, ask:

Schema

  • Are tables modeled around real business entities?
  • Are relationships clear?
  • Are data types appropriate?
  • Are unnecessary duplicated fields avoided?

Queries

  • Are queries retrieving only the required data?
  • Are expensive joins understood?
  • Are N+1 queries eliminated?
  • Are queries analyzed with execution plans?

Indexes

  • Are frequently queried columns indexed?
  • Are composite indexes used where appropriate?
  • Are there unnecessary indexes slowing down writes?

Integrity

  • Are unique constraints enforced?
  • Are foreign keys appropriate?
  • Are transactions used where operations must succeed together?
  • Are concurrency problems considered?

API

  • Is pagination implemented?
  • Are large datasets protected from unrestricted queries?
  • Are retries handled safely?
  • Are idempotency keys used where necessary?

Operations

  • Is database performance monitored?
  • Are slow queries tracked?
  • Are backups tested?
  • Is data retention defined?
  • Are migrations designed for production?

Growth

  • What happens when the database reaches 10 million rows?
  • What happens at 100 million?
  • Which tables will grow fastest?
  • Which queries will become expensive first?

These questions are far more valuable than simply asking:

"What database should I use?"


The 10,000-User Test

Here's a simple exercise I recommend.

Take your application's most important database tables.

Imagine:

10,000 users

Then:

100,000 users

Then:

1,000,000 users

Now ask:

  • How large will the transactions table become?
  • Which columns will be queried most frequently?
  • Which queries will run on every dashboard load?
  • Which indexes will be required?
  • Which records can be archived?
  • How large will the logs become?
  • How many concurrent database connections will exist?
  • What happens during a traffic spike?
  • Which tables will receive the most writes?
  • What happens when two requests modify the same record simultaneously?

You don't need to build for one million users today.

You need to understand what your current design will look like when the data becomes large.

That understanding allows you to make better decisions today.


Scalability Is About Bottlenecks

There's another important lesson here.

Your system doesn't scale according to its strongest component.

It scales according to its bottleneck.

You might have:

Excellent frontend
+
Excellent API
+
Excellent infrastructure
+
Excellent caching
+
Weak database design

The database can still become the limiting factor.

Likewise:

Excellent database
+
Poor API architecture

can still produce a slow application.

Scalability is therefore a systems problem.

Every layer interacts with the others.


The Database Doesn't Have to Be Fast Everywhere

This may sound strange, but not every database operation needs to be optimized for maximum speed.

Some operations can tolerate seconds.

Others require milliseconds.

For example:

User login
→ Fast

Payment authorization
→ Fast and reliable

Analytics report
→ May tolerate slower processing

Monthly financial report
→ Could potentially run asynchronously

Historical export
→ May take minutes

Trying to make every operation instantaneous can lead to unnecessary complexity.

Instead, classify workloads based on their requirements.

Some need:

Low latency

Others need:

High throughput

Others need:

Strong consistency

Others can tolerate:

Eventual consistency

Engineering is about matching the system to the requirement.


Final Thoughts: Data Eventually Wins

Applications come and go.

Features change.

Frameworks change.

Cloud providers change.

But one thing remains:

Data accumulates.

Every successful application creates more of it.

More users create more records.

More transactions create more history.

More features create more relationships.

More years create more complexity.

This means database scalability isn't something you solve once.

It is something you continuously manage.

The database that works perfectly during your first month of development may behave very differently after five years of production data.

The developers who understand this don't wait for the database to become slow before thinking about scale.

They design with growth in mind.

They monitor real workloads.

They inspect queries.

They understand indexes.

They protect consistency.

They plan migrations.

They control data growth.

And most importantly, they recognize that performance isn't just about how quickly a query runs today.

It is about how that query behaves when the database contains ten times, one hundred times, or one thousand times more data.

Because eventually, every successful application reaches the same question:

"What happens when there is a lot more data than we have today?"

The answer shouldn't be:

"We'll figure it out when we get there."

It should already be part of the engineering conversation.


What Comes Next?

We've now covered three critical layers of scalable software.

Part I — The Foundation

We explored the engineering mindset, technical debt, the different dimensions of scalability, and why software should be designed for growth rather than rewritten because of it.

Part II — The Architecture

We examined coupling, cohesion, monoliths, modular monoliths, microservices, APIs, events, business domains, and the trade-offs behind architectural decisions.

Part III — The Database

We went beneath the application layer and examined schema design, indexes, queries, transactions, concurrency, pagination, caching, replication, data growth, and database observability.

But there is still another major problem.

Even if your application is architecturally sound and your database is properly designed, you can still bring the entire system down by making one mistake:

Doing too much work synchronously.

Sending emails.

Processing images.

Generating reports.

Calling third-party APIs.

Sending notifications.

Processing webhooks.

Generating invoices.

Running analytics.

None of these tasks necessarily need to happen while the user waits for an HTTP response.

And when thousands of users start performing these operations simultaneously, synchronous processing can become one of the biggest bottlenecks in the entire system.

That brings us to the next layer of scalability:

Queues, background jobs, asynchronous processing, and event-driven systems.

Because sometimes the best way to make software faster isn't to make the work faster.

It's to stop making the user wait for it.

Full Stack Development, SoftwareEngineering, #kingsleyanusiem, #kingtech
25 min read
Sep 17, 2026
By Kingsley Anusiem
Share

Leave a comment

Your email address will not be published. Required fields are marked *

Related posts

Jul 12, 2026 • 10 min read
Building Software That Doesn't Break at 10,000 Users (Part II)

Part II — Designing the Architecture Before Writing the Code "Most software doesn't become difficul...

Jul 12, 2026 • 9 min read
Building Software That Doesn't Break at 10,000 Users

Part I — The Foundation of Scalable Software "Software rarely breaks because too many people use it...

May 20, 2026 • 16 min read
Writing Code That Scales: A Deep Dive for Developers

Most systems don't fail because developers wrote bad code. They fail because developers wrote code t...