← Back to Blog

Building Scalable Apps Without Over-Engineering

Every developer has read the horror stories: a startup that spent 6 months building a microservices architecture before launching, only to find they had fewer than 100 users. Or the team that chose Kafka for a message queue that handles 10 events per day.

Over-engineering is seductive. It feels like good work. It looks impressive in architecture diagrams. But it kills momentum, delays launches, and often produces systems that are harder to understand and operate than the simple version would have been.

Start with a Monolith

The default recommendation from experienced engineers has shifted back to the monolith in recent years — and for good reason. A well-structured monolith is easier to build, deploy, debug, and reason about than a distributed system. It's also easier to extract services from later, when you actually understand your domain boundaries.

The teams that successfully migrate to microservices usually started with a monolith. The teams that started with microservices usually regret it.

What "well-structured monolith" actually means:

  • Clear module boundaries enforced in code, even if not in deployment
  • Domain logic separated from infrastructure concerns
  • No circular dependencies between modules
  • A single deployable unit that can be tested end-to-end

Scale the Database Last

Most applications are bottlenecked on the database before they're bottlenecked on application servers. And most database bottlenecks can be solved without changing architecture:

  • Add indexes. Missing indexes on frequently-queried columns are the most common database performance issue we see.
  • Use a read replica. Separating reads from writes is often enough to handle 10x more traffic.
  • Add a cache. Redis in front of hot query results dramatically reduces database load.
  • Optimise queries. N+1 queries are silent killers. Use query analysers and connection pool metrics.

Only after these are exhausted should you consider sharding, partitioning, or switching database engines.

The Right Abstractions at the Right Time

Abstraction is how we manage complexity. But premature abstraction adds complexity rather than removing it. The trick is knowing when a pattern genuinely simplifies your code versus when it just makes it look more "engineered".

Three repetitions is the minimum before abstracting. One use case is never an abstraction. Two is a coincidence. Three is a pattern.

This applies to everything: helper functions, service classes, API clients, configuration patterns. If you've only used it once, you don't know enough about its shape to abstract it well.

Async is Not Always Faster

Async/await and event-driven architectures are powerful — but they add complexity. Use them where they genuinely help:

  • I/O-bound work that would block a thread (external API calls, database queries)
  • Background processing that doesn't need to complete in-request
  • Fan-out scenarios where multiple independent operations happen in parallel

For CPU-bound work, async doesn't help and can hurt. For simple sequential operations, the overhead of a queue and worker process is rarely worth it at small scale.

Observability from Day One

The one area we never compromise on is observability. Structured logs, request tracing, and basic metrics are not premature optimisation — they are the minimum infrastructure for operating a production system.

Without them, you're flying blind when something goes wrong. With them, most production issues are diagnosable in minutes rather than hours.

You don't need a complex observability stack on day one. Structured JSON logs shipped to a log aggregator, error tracking (Sentry or similar), and one dashboard with key metrics (latency, error rate, throughput) is enough to start.

The Scalability Test

Before adding architectural complexity, ask: "What's the simplest change that would handle 10x my current load?" Usually the answer is a single well-placed index, a cache, or a read replica — not a rewrite.

Build for where you are. Optimise for where you're going. Don't architect for where Amazon is.

If you're designing a new system or refactoring an existing one, talk to us. We've helped teams untangle over-engineered systems and build leaner, faster products.