Not Every Problem Needs a Microservice, a Queue, or a Cache
by Arif Ikhsanudin, Backend Developer
The Pattern-First Trap
A junior-to-mid engineer reads about microservices, message queues, and caching. They understand how each one works. They encounter a new problem. They fit the problem to the pattern rather than fitting the pattern to the problem.
The result is a system that looks sophisticated in architecture diagrams and is painful to operate. Microservices that should be a module. A Kafka topic for events that happen twice a day. A Redis cache with a 30-second TTL on data that changes every 15 seconds.
Each of these patterns has real value. Each is also misapplied more often than it is applied correctly. The skill is knowing which problems actually call for which patterns — and having the discipline to use simpler solutions when the pattern isn't justified.
The Microservice Default
Microservices solve specific problems: independent deployment of components that change at different rates, independent scaling of components with different load profiles, isolation of components with different reliability requirements, team autonomy for large organizations where multiple teams can own separate services.
They are routinely applied to problems that none of these justify. A five-person team building a product with one deployment environment doesn't benefit from microservices. They pay the full cost — distributed tracing, service-to-service authentication, network latency on what were function calls, independent deployment pipelines, testing across service boundaries — without any of the benefits, because the benefits are about organizational scale and independent deployment, not about technical correctness.
The monolith-first approach — build as a modular monolith, extract services when the justification is concrete — is both well-documented as a pattern (Sam Newman's Building Microservices endorses it) and consistently more pragmatic for small-to-medium products and teams.
The Queue Default
Message queues earn their place when temporal decoupling matters: when the producer shouldn't have to wait for the consumer, when consumers need to process at a different rate than producers emit, when at-least-once delivery semantics are required for reliability.
They are misapplied when:
- The operation is synchronous by necessity (the user is waiting for a result)
- The event volume is low enough that a database job table would serve identically with less operational overhead
- The "decoupling" being achieved is between two parts of the same service that could just be function calls
A database table with a status column and a background worker polling it is not glamorous. For moderate event volumes (under a few hundred per second), it is often more operationally reliable, easier to monitor, and easier to debug than a message broker. This pattern — sometimes called "transactional outbox" — avoids the dual-write problem and requires no new infrastructure. For many use cases, it should be the default.
The Cache Default
The cache is the most reflexively applied pattern of the three. Slow? Add a cache. High database load? Add a cache. Expensive computation? Add a cache.
Caching is only useful when:
- The same data is read significantly more often than it changes
- The cache hit rate under real traffic will be meaningfully high
- The staleness window is acceptable for the data being cached
A cache that is invalidated on every write is not a cache — it's latency. A cache with a 60-second TTL on data that changes every 30 seconds will serve stale data half the time. A cache on a page that generates unique content per user has near-zero hit rate.
Before adding caching, measure: what is the actual read-to-write ratio for this data? What TTL would be necessary to achieve an 80%+ hit rate? Is that TTL acceptable given the staleness implications?
The Right Default
The right default for most backend problems is the simplest design that correctly handles the required functionality at the current scale. A well-indexed PostgreSQL table with a background job. A service with a clear module structure rather than separate deployed services. A query result returned directly rather than cached.
Add the pattern when you can name the specific problem it solves. Not "this is how it's done" — the specific problem.
Microservices: when you need independent deployment or scaling of this specific component. Queues: when you need temporal decoupling, at-least-once delivery, or rate leveling for this specific integration. Caches: when this specific data has a read/write ratio and staleness tolerance that makes caching beneficial.
The Practical Takeaway
Before adding any of these three patterns to your next design, write one sentence that describes the specific problem you have today that requires it. "This is the right architecture" is not a problem statement. "The notification service causes payment timeouts when it's slow" is a problem statement that justifies a queue. If you can't complete the sentence, use the simpler design.