cognitive-horizon
MMXXVI
Architecture · · By Art

Choosing Your Architecture: A Decision Framework

The architecture question comes up in every project: monolith or microservices? Event-driven or request-response? Serverless or containers? After 25 years of building production systems, I've found that the answer is almost never about the technology itself — it's about the constraints of your situation. Here's the framework I use.

What we'll cover

  1. The three variables that determine your architecture
  2. The Monolith: still the right default
  3. The Modular Monolith: boundaries without the network
  4. Microservices: when the pain is worth it
  5. Event-Driven Architecture: decoupling through time
  6. Serverless: paying for what you use
  7. The decision matrix

The Three Variables That Determine Your Architecture

Before evaluating any architecture pattern, answer three questions:

1. Team size and structure. How many developers work on the system? Are they in one team or many? Do they deploy independently? A monolith is natural for one team. Microservices make sense when multiple teams need to ship independently.

2. Domain complexity. Is the business logic simple (CRUD with some rules) or genuinely complex (multiple interacting workflows, complex state machines, domain-specific calculations)? Simple domains don't need complex architectures. Complex domains need clear boundaries.

3. Scale requirements. Not hypothetical scale — actual, measured scale. How many requests per second? How much data? What are the latency requirements? Most applications never need to scale beyond what a single well-configured server can handle.

Most architecture mistakes come from overweighting one variable and ignoring the others. A two-person startup adopting microservices because "they might need to scale" is optimizing for variable 3 while making variable 1 (team productivity) dramatically worse.

The Monolith: Still the Right Default

A well-structured monolith is the right choice for the vast majority of applications. One deployment unit, one database, one codebase. Simple to develop, simple to debug, simple to deploy. No network calls between components, no distributed transactions, no service discovery.

When it works: Team size under ~15. Domain complexity is manageable. Scale requirements are under ~10,000 requests per second (which covers most applications). You want to move fast and iterate.

When it doesn't: Multiple teams stepping on each other's deployments. The codebase has grown so tangled that changes in one area routinely break another. You need to scale different parts of the system independently (e.g., the search indexer needs 10x the compute of the web server).

The key: A monolith only works if it's well-structured. A ball of mud with no internal boundaries is painful at any scale. If you're building a monolith, invest in clean module boundaries, clear dependency rules, and comprehensive tests.

The Modular Monolith: Boundaries Without the Network

The modular monolith is the architecture pattern I recommend most often. It's a monolith with strict internal boundaries — each module owns its data, exposes a defined interface, and communicates with other modules through those interfaces rather than reaching directly into their internals.

Think of it as microservices without the network. You get the modeling benefits of clear boundaries and independent modules, but you keep the operational simplicity of a single deployment. You can refactor across module boundaries with your IDE rather than coordinating multiple service deployments.

The migration path: If you eventually need microservices, a modular monolith is the ideal starting point. Each module already has a defined interface and owns its data. Extracting a module into a service is a deployment change, not an architecture change.

When it works: Medium to high domain complexity. One to several teams. You want boundary discipline without distributed systems overhead. This is the sweet spot for most growing applications.

Microservices: When the Pain Is Worth It

Microservices solve a specific problem: enabling multiple teams to develop and deploy independently. That's it. They are not about "scaling" (you can scale a monolith horizontally), "technology choice" (you can use multiple languages in a monolith), or "resilience" (a microservices system has more failure modes, not fewer).

The cost of microservices is real and ongoing: network latency between services, distributed transaction complexity, operational overhead of running and monitoring multiple services, data consistency challenges, and the cognitive load of understanding a distributed system.

When it works: Large organization (50+ engineers). Multiple teams that need independent deployment cycles. Different parts of the system have genuinely different scaling needs. You have the infrastructure team and operational maturity to manage distributed systems.

The honest assessment: Most teams that adopt microservices would be more productive with a modular monolith. The teams that genuinely benefit from microservices usually arrived there by extracting services from a monolith that outgrew its boundaries — not by starting with microservices on day one.

Event-Driven Architecture: Decoupling Through Time

Event-driven architecture decouples producers from consumers by introducing a time gap between "something happened" and "something reacts to it." Instead of Service A calling Service B directly, Service A publishes an event ("OrderPlaced") and Service B subscribes to it. They don't need to know about each other.

When it works: Multiple independent reactions to the same event (an order triggers payment, inventory update, notification, and analytics — each independently). The producer shouldn't be blocked by slow consumers. You need an audit trail of everything that happened.

When it doesn't: When you need a synchronous response ("tell me if the payment succeeded right now"). When the event ordering matters and your system can't handle out-of-order processing. When the debugging complexity of async event chains outweighs the decoupling benefit.

The hybrid approach: Most real systems use event-driven architecture for some workflows and request-response for others. The user-facing request path is typically synchronous (you need to tell the user what happened), while background processing and cross-system communication are event-driven.

Serverless: Paying for What You Use

Serverless (Lambda, Cloud Functions, Workers) abstracts away all infrastructure. You deploy functions, the cloud provider runs them, and you pay per invocation. No servers to manage, no capacity to plan.

When it works: Highly variable traffic with significant idle periods. Event processing (file uploads, webhook handlers, scheduled jobs). APIs with low to moderate traffic where you want zero operational overhead. Prototyping and MVPs.

When it doesn't: High-throughput, low-latency workloads (cold starts kill you). Long-running processes. Applications with heavy state that doesn't fit the stateless function model. When the cost-per-invocation exceeds the cost of a dedicated server at your traffic level.

The Decision Matrix

Here's how I make the decision in practice:

Situation Architecture
Small team, new product, need to iterate fast Monolith
Growing complexity, want future flexibility Modular Monolith
Multiple teams, independent deploy needed Microservices
Many independent reactions to business events Event-Driven (often hybrid)
Variable traffic, minimal ops team Serverless
Unsure Start with a modular monolith

Key Takeaways

  • 1. Architecture decisions are driven by three variables: team size, domain complexity, and scale requirements. Evaluate all three, not just the one that excites you.
  • 2. The monolith is still the right default for most applications. A well-structured monolith beats a poorly-operated microservices system every time.
  • 3. The modular monolith is the sweet spot for most growing applications. Boundary discipline without distributed systems overhead.
  • 4. Microservices solve an organizational problem (independent team deployment), not a technical one. Don't adopt them for technical reasons alone.
  • 5. Most real systems are hybrids. Synchronous request-response for user-facing paths, event-driven for background processing. Don't force everything into one pattern.
  • 6. When in doubt, start simpler. It's easier to extract a service from a well-structured monolith than to merge poorly-designed microservices.

Need help with your architecture?

We help teams choose and implement the right architecture for their situation.