Software Modernisation · 14 min read

Software Architecture Patterns: A Practical Guide

Major software architecture patterns compared. Monolith, microservices, modular monolith, event-driven, serverless: trade-offs and when each makes sense.

Architecture is the set of fundamental decisions about how your software is structured. Get it right, and development flows smoothly for years. Get it wrong, and your team spends more time fighting the architecture than building features.

There's no universally "best" architecture. Each pattern makes trade-offs. This guide covers the major patterns, their honest trade-offs, and when each actually makes sense.

Monolithic architecture

A single deployable unit containing all application functionality. One codebase, one deployment, one database. Despite the industry's obsession with microservices, monoliths remain the right choice for most applications, especially early on.

Advantages

  • Simplicity. One codebase to understand, one thing to deploy.
  • Easy debugging. A single process. You can step through the entire request path in your debugger.
  • No network overhead. Components communicate via in-process function calls: nanoseconds, not milliseconds.
  • Simple transactions. ACID transactions across all features, no distributed transaction headaches.
  • Lower operational cost. No service discovery, no API gateway, no distributed tracing infrastructure.

Disadvantages

  • Scaling is all-or-nothing. You scale the whole application even if only one component is under load.
  • Deployment risk. Every change requires redeploying everything. A bug in billing takes down the whole app.
  • Technology lock-in. One language, one framework for the entire application.
  • Team friction at scale. With 30+ developers working in one codebase, merge conflicts and coordination overhead become significant.

Best for: New projects, small-to-medium teams (under 10 developers), moderate complexity, MVPs and rapid prototyping. Start here unless you have clear, proven reasons not to.

Microservices

The application is decomposed into small, independently deployable services. Each service owns its data, has its own database, and communicates with others via APIs or message queues.

Advantages

  • Independent deployment. Ship changes to one service without touching others.
  • Technology flexibility. Each service can use whatever language or framework fits best.
  • Targeted scaling. Scale only the services that need more resources.
  • Team autonomy. Teams own services end-to-end, reducing coordination overhead.
  • Fault isolation. A failing service doesn't necessarily bring down the entire application.

Disadvantages

  • Operational complexity. Dozens of services to deploy, monitor, log, and troubleshoot.
  • Network latency. Every service call crosses the network. Latency adds up across call chains.
  • Distributed data. No simple transactions across services. Eventual consistency, saga patterns, compensating actions.
  • Testing difficulty. Integration testing across services requires sophisticated infrastructure.
  • Debugging is hard. A single user request might touch 15 services. You need distributed tracing (Jaeger, Zipkin) to follow it.

Best for: Large teams (20+ developers), complex domains with natural service boundaries, organisations with mature DevOps and observability practices. If you don't have strong CI/CD, monitoring, and incident response, microservices will hurt more than help.

Modular monolith

The pragmatic middle ground. A single deployable application with strictly enforced internal module boundaries. Each module owns its own database tables and communicates with others through defined interfaces, not direct database queries or shared models.

You get the modularity benefits of microservices (clear boundaries, team ownership, independent reasoning) without the distributed systems costs (network latency, eventual consistency, operational complexity).

Key principles

  • Each module exposes a public API that other modules call
  • Modules never access each other's database tables directly
  • Modules can be extracted to independent services later if there's a genuine need
  • One deployment, but the internal organisation maps to potential service boundaries

Best for: Teams who want clear boundaries and modularity without the pain of distributed systems. Often the right architecture for medium-sized applications (5–20 developers) and a natural stepping stone toward services if and when the need is proven.

Layered architecture

Organises code into horizontal layers: presentation (UI), business logic, data access. Each layer depends only on the layer below it. This is the classic n-tier approach.

Layer Responsibility
Presentation UI rendering, API endpoints, input validation
Business logic Core rules, workflows, domain operations
Data access Database queries, ORM mapping, persistence
Database Data storage and retrieval

Best for: CRUD-heavy applications, teams familiar with MVC patterns, projects where the domain logic is straightforward. The default architecture for most web frameworks out of the box.

Event-driven architecture

Components communicate by publishing and subscribing to events rather than calling each other directly. When something happens (order placed, user registered, payment received), an event is published. Interested components react independently.

Event sourcing

A variant where system state is stored as a sequence of events, not as current values. To determine current state, replay all events from the beginning. This gives you complete audit history and enables powerful temporal queries ("what did the order look like at 3pm Tuesday?"). The trade-off: read models need to be built separately, and the programming model takes time to learn.

CQRS

Command Query Responsibility Segregation separates read and write paths into different models. Writes go through a command model optimised for validation and business rules. Reads use query models optimised for the specific views users need. Often combined with event sourcing.

Best for: Applications with complex domain logic, strong audit requirements, or high-throughput scenarios where read and write patterns are very different. Higher complexity. Don't adopt without clear benefits that justify the learning curve.

Serverless

Functions deployed and executed on-demand, with the cloud provider handling all infrastructure. No servers to provision, patch, or scale. You pay only for execution time.

Advantages

  • Zero infrastructure management. AWS Lambda, Azure Functions, Google Cloud Functions handle scaling, patching, availability.
  • Pay-per-execution. No cost when no one's using it. Ideal for variable workloads.
  • Instant scaling. Handles traffic spikes without any configuration.

Disadvantages

  • Cold starts. First invocation after idle can take several seconds: problematic for latency-sensitive use cases.
  • Execution constraints. Timeouts (typically 15 min max), memory limits, no persistent local state.
  • Vendor lock-in. Your architecture becomes tightly coupled to a specific cloud provider's function runtime.
  • Debugging and observability. Distributed execution across many small functions is difficult to trace without good tooling.

Best for: Event-driven workloads (webhook handlers, file processing), variable traffic patterns, API backends for mobile apps, glue code between services. Startups who want minimal ops overhead.

Choosing the right architecture

Five questions that steer the decision:

  1. How complex is the domain? Simple CRUD → monolith. Complex business rules with distinct bounded contexts → modular monolith or microservices.
  2. How large is the team? Under 8 developers → monolith works well. 8–20 → modular monolith. 20+ → consider microservices along team boundaries.
  3. What are the scaling requirements? Uniform load → monolith is fine. Different components with vastly different scaling profiles → services.
  4. What's your operational maturity? Microservices require CI/CD, monitoring, distributed tracing, and incident response to be mature. Without these, microservices create more problems than they solve.
  5. How much is known upfront? High uncertainty (new product, new market) → start simple and evolve. Premature architecture decisions based on assumptions waste time.

Frequently asked questions

Should we use microservices?

Probably not, unless you have a large team (20+), a complex domain with clear service boundaries, and mature DevOps practices. Most applications that adopt microservices prematurely end up with a "distributed monolith", all the complexity of both approaches with the benefits of neither.

Can we change architecture later?

Yes, and you should expect to. Architecture isn't a one-time decision. It evolves as you understand the problem better and the organisation grows. A well-structured monolith with clean boundaries (modular monolith) gives you the option to extract services later without requiring it upfront.

What about hexagonal architecture / clean architecture?

These are complementary to the patterns above. They describe how to organise code within a service or application rather than how services relate to each other. Hexagonal architecture (ports and adapters) works well inside a monolith, a module, or a microservice.

Key takeaways

  • There's no universally "best" architecture. Only the right fit for your team size, domain complexity, and operational maturity.
  • Start simpler than you think you need. Extracting services from a well-designed monolith is easier than merging poorly-conceived microservices.
  • The modular monolith gives you microservices-style boundaries without distributed systems complexity. Often the sweet spot.
  • Architecture evolves. What's right for year one may not be right for year five. Design for change.
Kasun Wijayamanna
Kasun Wijayamanna Founder & Lead Developer

Postgraduate Researcher (AI & RAG), Curtin University - Western Australia

View profile →

Ready to discuss your project?

Tell us what you're working on. We'll come back with a practical recommendation and clear next steps.