Microservices vs Monolith: Which to Choose?
Microservices vs Monolith: Which to Choose?
The decision between microservices and monolithic architecture shapes every dimension of your development workflow — from how quickly your team ships features to how much you spend on infrastructure. Teams that choose microservices too early burn engineering time on distributed system problems before they have product-market fit. Teams that stick with monoliths too long hit scaling walls that require months of rewrite work. The cost of choosing wrong compounds over time.
This article provides a decision framework grounded in specific team size thresholds, traffic patterns, and organizational constraints. You'll learn when monoliths genuinely outperform microservices, what triggers should prompt migration, and how to avoid the most common failure modes of both approaches. The analysis draws on production architecture patterns from systems handling everything from 100 to 100 million requests per day.
We'll cover the technical tradeoffs, team structure implications, and specific decision points that make one architecture clearly superior for your context.
Understanding Monolithic Architecture
A monolithic application deploys as a single unit — one codebase, one deployment process, one runtime process (or multiple instances of that same process). All features share the same database, memory space, and release cycle. When you deploy a change to the authentication module, you redeploy the entire application including the payment processing, user management, and reporting modules.
This shared-everything model creates both significant advantages and specific constraints. The primary advantage is simplicity: you have one application to build, test, and deploy. Cross-cutting concerns like logging, monitoring, and authentication configuration happen once. Adding a new feature means writing code in the same repository using the same patterns your team already knows.
The constraint emerges from coupling. Changes to one module can break another. A memory leak in the reporting feature affects the payment processing feature. Scaling requires scaling the entire application even if only one feature needs more capacity. Teams working on different features must coordinate deployments because they share the same release artifact.
When Monoliths Excel
Monolithic architecture delivers optimal results for specific scenarios that more teams encounter than the industry narrative suggests. If your entire engineering team fits in one Slack channel (under 15 developers), the coordination overhead of microservices typically exceeds the benefits. You spend more time managing service boundaries, deployment pipelines, and network reliability than you gain from independent deployability.
Early-stage products benefit from monoliths because feature boundaries shift rapidly. What looks like two separate services today might merge tomorrow based on user research. Refactoring across service boundaries requires coordination across teams and careful API versioning. Refactoring within a monolith is a single pull request. When you're still figuring out what to build, this flexibility matters more than scaling characteristics.
Data consistency requirements also favor monoliths. If your business logic requires ACID transactions across multiple entities — order processing that updates inventory, creates shipment records, and charges payment methods — handling this in a monolith means using database transactions. In microservices, you implement distributed transaction patterns like sagas, which add significant complexity and create failure modes that don't exist in single-database systems.
The default architecture for any new product should be a well-structured monolith. Microservices are an optimization for specific scaling problems, not a starting point. Teams that begin with microservices before they have measurable scaling problems waste 6-12 months building infrastructure that provides no user value.
The Monolith Scaling Path
Well-designed monoliths scale further than most teams realize. Shopify runs on a Rails monolith that handles Black Friday traffic spikes. Stack Overflow serves millions of developers from a .NET monolith. The key is understanding which scaling strategies apply to single-deployment architectures.
Horizontal scaling — running multiple instances of the application behind a load balancer — works until around 50-100 instances. Beyond that, deployment coordination and cache invalidation across instances becomes unwieldy. Vertical scaling — using larger servers — works until you hit the memory or CPU limits of available instance types. Caching strategies (Redis, CDN) can extend both limits significantly.
The actual scaling ceiling depends on your bottleneck. CPU-bound applications (image processing, video encoding) scale horizontally until deployment complexity overwhelms your team. Database-bound applications hit limits based on your database's write throughput and connection pool management. Most web applications are database-bound, which means monolith scaling is really about database scaling strategies: read replicas, connection pooling, query optimization, and caching.
Understanding Microservices Architecture
Microservices architecture decomposes an application into independently deployable services, each owning its data and exposing functionality through network calls. The payment service runs in separate processes from the user service, maintains its own database, and communicates over HTTP or message queues. Teams can deploy changes to the payment service without touching the user service deployment.
This independence creates the primary value proposition: different teams can move at different speeds using different technology stacks. The payment team can use Java for transaction processing while the notification team uses Node.js for high-concurrency I/O. The recommendation engine can deploy five times per day while the billing system deploys monthly. Each service scales based on its own load characteristics.
The cost is distributed system complexity. Network calls replace function calls, which means handling timeouts, retries, and partial failures. Data consistency requires distributed transaction patterns because you can't use database transactions across services. Debugging issues requires tracing requests across multiple services. Local development environments need to run multiple services simultaneously, which often requires Docker Compose or Kubernetes locally.
When Microservices Win
Microservices become the optimal choice when specific organizational or technical constraints make monolithic deployment impractical. The clearest signal is team size: once you have more than 15-20 developers working on the same codebase, merge conflicts and deployment coordination overhead create measurable friction. Different teams need to deploy independently to maintain velocity.
Differential scaling requirements provide another clear trigger. If your application includes features with 100x traffic differences — an analytics dashboard that serves 100 requests per minute and a public API that serves 10,000 — running them in the same deployment means overprovisioning the dashboard to handle the API load. Splitting them into separate services lets you scale each appropriately, which can reduce infrastructure costs by 40-60%.
Technology heterogeneity becomes valuable when different features have genuinely different technical requirements. A machine learning recommendation engine benefits from Python's data science ecosystem. A high-frequency trading component needs Go's concurrency primitives. A batch processing job works better in Java's JVM ecosystem. Forcing all features into one language stack means using suboptimal tools for some use cases.
The most common microservices failure mode is premature decomposition. Teams split services along what seem like logical boundaries before understanding actual access patterns. This creates chatty inter-service communication where Service A calls Service B which calls Service C for every request, turning one local function call into three network round trips. Wait until you have real traffic data before drawing service boundaries.
The Microservices Tax
Microservices impose infrastructure and operational overhead that teams often underestimate. Each service needs its own deployment pipeline, monitoring dashboards, log aggregation, and error tracking. A system with 10 services requires 10 times the deployment infrastructure of a monolith. Teams spend 20-30% of their engineering time on infrastructure concerns that wouldn't exist in a monolithic deployment.
Network reliability becomes a constant concern. When Service A calls Service B, that call can timeout, fail, or return an error. Your code must handle each case gracefully. Circuit breaker patterns prevent cascading failures. Retry logic with exponential backoff handles transient errors. Request tracing helps debug issues that span multiple services. None of this complexity exists when the same functionality is a function call in a monolith.
Data consistency across services requires implementing distributed transaction patterns. The Saga pattern — where each service performs local transactions and publishes events that trigger the next step — works but creates failure modes that don't exist in traditional database transactions. You need compensation logic for every step in case later steps fail. Testing these failure scenarios requires complex integration test setups.
Decision Framework: When to Choose Which
The architecture decision depends on measuring specific characteristics of your team and product against known threshold values. These thresholds come from analyzing production systems across hundreds of companies, not theoretical ideals.
| Factor | Choose Monolith If | Choose Microservices If |
|---|---|---|
| Team Size | Fewer than 15 developers | More than 20 developers across multiple teams |
| Product Maturity | Pre-launch or finding product-market fit | Established product with stable domain boundaries |
| Deployment Frequency | All teams deploy on similar schedules | Different features need different deployment cadences |
| Scaling Pattern | All features have similar load characteristics | Features have 10x+ differences in load patterns |
| Transaction Needs | Frequent multi-entity ACID transactions required | Eventual consistency acceptable for most operations |
| Operational Maturity | Limited DevOps expertise or tooling | Strong DevOps team with container orchestration |
The 15-Developer Threshold
Team size creates the most reliable decision signal because it directly correlates with coordination overhead. With fewer than 15 developers, everyone knows what everyone else is working on. Merge conflicts are rare. Deployment coordination happens in a single Slack channel. The cognitive overhead of understanding the entire system fits in one team's collective knowledge.
Between 15-20 developers, you hit the first coordination problems. Multiple features deployed simultaneously start causing conflicts. Teams begin requesting "deployment windows" to avoid breaking each other's work. Code review bottlenecks emerge because senior developers become gates for too many changes. These are symptoms that monolithic deployment is becoming a constraint.
Beyond 20 developers, you typically have distinct teams (frontend, backend, infrastructure) or feature teams (payments, user management, analytics). Each team wants independent velocity. Microservices architecture aligns with this organizational structure by giving each team ownership of specific services they can deploy independently. The architecture matches the team topology.
Traffic Pattern Analysis
Examine the request distribution across your application's features. If 90% of traffic hits 10% of endpoints, you have a clear microservices candidate. Extract the high-traffic endpoints into a separate service that you can scale aggressively while keeping the low-traffic features in a cost-efficient monolith.
Look for seasonal or event-driven spikes that affect specific features. A tax preparation application sees 10x traffic from January to April. A retail system spikes during holiday sales. If these spikes affect the entire application uniformly, scale the monolith. If they affect specific features (checkout but not account settings), extract those features into independently scalable services.
Calculate your actual infrastructure costs for different scenarios. Run the numbers: current monolith cost versus projected microservices cost including the operational overhead. Teams often discover that running five services costs 2-3x more than running one monolith at the same scale due to fixed costs per service (load balancers, monitoring, CI/CD pipelines). Microservices save money when differential scaling reduces total compute costs more than the per-service overhead adds.
Hybrid Approaches: Modular Monoliths
The modular monolith provides a middle path that captures many microservices benefits while maintaining single-deployment simplicity. You structure code into well-defined modules with explicit interfaces, but deploy everything together. Each module owns its data models and business logic, communicating with other modules through defined boundaries, not direct database access or function calls into private code.
This approach gives you clean boundaries that make future extraction straightforward. When a module genuinely needs independent scaling or deployment, converting it to a microservice becomes a deployment change rather than a rewrite. The code is already decoupled. You change inter-module communication from function calls to HTTP calls and deploy the module separately.
Shopify's modular monolith runs on this pattern. Their architecture defines bounded contexts (checkout, inventory, shipping) as separate modules within the Rails application. Each module has its own database tables, service classes, and API boundaries. Teams can work on different modules with minimal coordination. When they eventually extract services, the module boundaries become service boundaries.
Implementing Module Boundaries
Enforce module boundaries through directory structure and linting rules. In a Node.js application, create top-level directories for each module: /src/payments, /src/users, /src/notifications. Configure ESLint to prevent imports across module boundaries except through designated public interfaces. This prevents the accidental coupling that makes monoliths hard to split later.
Each module exposes a service layer that other modules consume. The payments module exports a PaymentService class with methods like processPayment() and refundPayment(). Other modules import and use this service but never import internal payment module classes or directly query payment database tables. This creates the same interface contract you would have with a microservice, but without network calls.
// Good: Using the public service interface
import { PaymentService } from '@/payments';
await PaymentService.processPayment({
amount: 1000,
currency: 'USD',
customerId: user.id
});
// Bad: Reaching into module internals
import { Payment } from '@/payments/models/Payment';
await Payment.create({ ... }); // Breaks module encapsulation
Data Access Patterns
Module boundaries must extend to database access. Each module owns specific tables and other modules cannot query them directly. If the user module needs payment information, it calls the payment service API rather than joining user and payment tables. This seems less efficient than a single SQL query, but it maintains the decoupling that makes future service extraction possible.
Accept that some queries become less efficient in exchange for clearer boundaries. A dashboard that shows user information with payment history might require two queries (one to user service, one to payment service) instead of a single join. The performance cost is usually negligible — microseconds to milliseconds — while the architectural benefit is substantial. If specific queries genuinely need join-level performance, use database views or materialized views that aggregate data from multiple modules.
Use database schemas (PostgreSQL) or database prefixes (MySQL) to enforce module data ownership. Create separate schemas like payments, users, orders and configure application database roles so the users module literally cannot query the payments schema tables. This makes violations impossible rather than just discouraged.
Migration Patterns: Monolith to Microservices
Migrating from monolith to microservices requires a strangler fig pattern: gradually extracting services while keeping the monolith running. You don't rewrite the entire application. You identify high-value extraction candidates, extract them one at a time, and route traffic appropriately. The monolith shrinks gradually until only core features remain.
Start with the highest-impact, lowest-risk extraction. Look for features that have clear boundaries, minimal dependencies on other features, and genuine reasons to exist independently (different scaling needs, different deployment cadence). Notification systems, reporting engines, and search functionality often make good first extractions because they consume data from other features but those features don't depend on them.
Avoid extracting foundational features first. Authentication, user management, and core domain models have dependencies from every other feature. Extracting them requires updating every part of the codebase to call the new service. Extract leaf-node features first — features that other features don't depend on. Build confidence with successful extractions before tackling complex ones.
The Strangler Fig Technique
The strangler fig pattern works by routing specific requests to the new microservice while leaving everything else in the monolith. Use a reverse proxy (nginx, API Gateway) to implement routing rules. Requests to /api/notifications/* route to the notification service. Everything else routes to the monolith. The monolith doesn't know the notification service exists.
For the extracted service to work, it needs access to shared data. The notification service needs user information to send emails. Three approaches handle this:
- Data replication: Sync user data to the notification service database via events or CDC (Change Data Capture). The notification service has its own user table that mirrors the monolith's user table. This creates operational overhead but provides the best performance and isolation.
- API calls back to monolith: The notification service calls the monolith's API to fetch user data when needed. Simple to implement but creates coupling and latency. Works well for low-frequency operations.
- Event-driven data access: The monolith publishes user change events. The notification service consumes these events and builds its own view of user data. Eventually consistent but architecturally cleaner than direct API calls.
Deployment and Rollback Strategy
Deploy the new microservice behind a feature flag that routes only a percentage of traffic to it. Start with 1% of notification requests going to the new service, 99% to the old monolith code path. Monitor error rates, latency, and correctness. Gradually increase the percentage as confidence grows.
Keep the monolith code path working for at least two weeks after reaching 100% traffic on the new service. This gives you instant rollback capability. If the microservice has issues, flip the feature flag to route traffic back to the monolith. Once the microservice proves stable in production, remove the monolith code path.
Expect the first extraction to take 2-3x longer than estimated. You're building patterns and infrastructure that don't exist yet: service templates, deployment pipelines, monitoring dashboards, inter-service communication patterns. The second extraction uses these patterns and goes faster. By the third or fourth extraction, the process becomes routine.
Cost Analysis: Real Infrastructure Numbers
Infrastructure costs shift dramatically between architectures. A monolith running on three $200/month servers costs $600/month. Five microservices each running on two $100/month servers costs $1,000/month. Add load balancers ($20/month each), managed databases per service ($50/month minimum), and monitoring costs ($30/service/month), and the microservices setup costs $1,350/month versus $600/month for the monolith.
This gap widens or narrows based on scaling patterns. If one microservice needs 10 servers while others need two, microservices reduce costs by avoiding overprovisioning. You run the high-traffic service on 10 servers and low-traffic services on two. The equivalent monolith runs 10 servers for everything, overprovisioning the low-traffic features. At this scale, microservices cost $2,200/month versus $3,000/month for the monolith.
Operational costs matter more than infrastructure costs. Each microservice needs monitoring, logging, error tracking, and deployment pipelines. If your team spends 40 hours per month managing infrastructure, and microservices increase that to 80 hours, the additional engineering cost ($8,000/month at $100/hour blended rate) dwarfs the $600/month infrastructure difference. Make the architecture decision based on total cost, not just server costs.
Database Costs in Different Architectures
Monoliths use one database, which means one set of fixed costs: one connection pool, one backup system, one monitoring setup. Microservices typically use one database per service, which multiplies these costs. Five services with separate databases mean five connection pools, five backup systems, five monitoring setups.
Managed database services charge per instance. AWS RDS costs minimum $15/month for the smallest PostgreSQL instance. Five microservices with separate databases cost $75/month just for databases versus $15/month for the monolith. At production scale, this might be $200/month per database ($1,000/month total) versus $200/month for one database.
The database-per-service pattern provides benefits that sometimes justify this cost: independent schema evolution, failure isolation, and appropriate database technology per service (PostgreSQL for transactional data, MongoDB for documents, Redis for caching). Calculate whether these benefits exceed the cost delta for your specific use case.
Team Structure Implications
Architecture and team structure must align or both suffer. Monolithic architecture works best with component teams: a frontend team, a backend team, a database team. Everyone works in the same codebase, so coordination through code review and shared standards works naturally. Features require coordination across teams but deployment is centralized.
Microservices architecture aligns with product teams: each team owns specific services that deliver complete features. The payment team owns the payment service, deployment pipeline, database, and monitoring. They make independent decisions about technology, deployment schedule, and architecture within their service boundaries. This autonomy justifies the microservices complexity.
Misalignment creates dysfunction. Microservices with component teams means the frontend team needs the backend team to deploy service changes, destroying the independent deployability benefit. Monoliths with product teams means multiple teams competing for deployment windows in the same codebase, creating coordination overhead that product teams specifically avoid.
Conway's Law in Practice
Conway's Law states that organizations design systems that mirror their communication structure. If you have three teams that don't communicate well, you'll build three loosely coupled systems. If you have one cohesive team, you'll build one integrated system. This isn't a choice — it's an observation about how organizations work.
Use this to your advantage. Design your team structure to match your desired architecture. If you want microservices, create product teams with full ownership of specific services. If you want a monolith, create component teams with shared ownership of the codebase. Trying to force microservices with component teams or monoliths with product teams fights against natural organizational dynamics.
Recognize that team structure is often the actual constraint. If you have one team and want to hire more people, monolithic architecture lets you scale by adding people to the team. Microservices architecture requires splitting into multiple teams, which requires more senior engineers who can work independently. The architecture decision is really a hiring and team structure decision.
Performance Characteristics
Monoliths deliver better performance for operations that need data from multiple features. A dashboard showing user profile, recent orders, payment methods, and notification preferences requires data from four modules. In a monolith, this is four table joins in one SQL query — microseconds of database time. In microservices, it's four HTTP requests to different services — 20-100 milliseconds including network latency.
This performance gap matters for user-facing operations. Every 100ms of latency reduces conversion rates by approximately 1% for e-commerce applications. If microservices add 80ms of latency to critical paths like checkout, that's measurable business impact. Caching helps but adds complexity and cache invalidation problems.
Microservices win performance comparisons when scaling is the bottleneck. If your monolith's CPU maxes out because the recommendation engine uses 80% of available CPU, the rest of the application suffers. Extracting the recommendation engine to its own service lets you scale it independently on CPU-optimized instances while running the rest of the application on cost-effective instances. This both improves performance and reduces costs.
Latency Budgets
Establish latency budgets for critical paths before choosing architecture. If your checkout flow must complete in under 500ms, and that flow currently touches five different modules, you have 100ms per module. In a monolith, this is achievable. In microservices, where each inter-service call adds 20-50ms of network latency, you're at your budget before doing any actual work.
Design service boundaries to minimize network hops on critical paths. Don't split features that always execute together into separate services. The shopping cart and checkout features always interact — putting them in separate services guarantees network latency on every checkout request. Keep them together and extract truly independent features like notifications or recommendations.
Amazon's analysis shows that every 100ms of latency costs them 1% in sales. For a service doing $100M annually, that's $1M per 100ms. If microservices add 200ms to critical paths, the architecture costs $2M/year in lost revenue — likely more than you save in infrastructure costs. Measure latency impact on business metrics, not just technical metrics.
Development Workflow Differences
Local development in a monolith means running one application. Clone the repository, install dependencies, start the application, and you have the complete system running locally. New developers set up their environment in under an hour. Debugging means setting breakpoints and stepping through code. All functionality is available without network configuration.
Local development in microservices means running multiple services simultaneously. Each service needs its own environment configuration, database setup, and dependency installation. Developers typically use Docker Compose to orchestrate 5-10 containers locally. New developer setup takes 4-8 hours and requires understanding container orchestration. Debugging across services requires distributed tracing tools.
This complexity multiplies throughout the development workflow. Testing a feature that spans three services requires starting all three services and their dependencies. Database migrations must be coordinated across services. Schema changes in one service that affect other services require careful versioning and deployment sequencing. What's a single pull request in a monolith becomes a coordinated deployment across multiple repositories in microservices.
Testing Strategies
Monoliths enable straightforward integration testing. Spin up the application and database, run your test suite, verify behavior end-to-end. Tests access the database directly to set up fixtures. All code is in the same runtime, so code coverage tools work naturally. The entire test suite completes in minutes.
Microservices require contract testing and complex test environments. Each service has unit tests, but integration testing requires either running all services (slow and brittle) or mocking service dependencies (fast but misses integration bugs). Contract testing verifies that services satisfy the interfaces other services expect, but this adds tooling complexity and requires discipline to maintain.
End-to-end testing in microservices means managing test data across multiple databases and coordinating state across services. If your test needs a user with an active subscription and recent payment, you must create that user in the user service, create a subscription in the subscription service, and create a payment record in the payment service. Monoliths set this up with one database insert or fixture file.
Monitoring and Debugging
Monolithic applications centralize monitoring. One application means one set of logs, one error tracking service, one performance monitoring dashboard. When an error occurs, the stack trace shows exactly what failed. Logs include the complete request context. You can query logs to find all requests from a specific user or all errors in a specific feature.
Microservices distribute monitoring across services. An error might originate in Service A, propagate through Service B, and surface in Service C. Understanding what happened requires correlating logs across all three services. This requires request tracing with correlation IDs — unique identifiers passed between services so you can follow a request's path through the system.
Tools like Jaeger, Zipkin, or cloud provider tracing (AWS X-Ray, Google Cloud Trace) help by visualizing request flows across services. But implementing distributed tracing requires instrumentation in every service, adding code to propagate trace context with every inter-service call. This infrastructure doesn't exist in monoliths because it isn't needed.
Debugging Distributed Failures
The hardest microservices bugs involve timing and network issues that don't reproduce locally. Service A times out calling Service B, but only under production load. Service C receives corrupted data from Service D, but only 0.1% of the time. These bugs require production-level traffic to surface and distributed tracing to diagnose.
Implement robust logging from the start. Every inter-service call should log the request ID, duration, response status, and any errors. Structure logs as JSON so you can query them efficiently. Use centralized log aggregation (ELK stack, CloudWatch Logs Insights) to search across all services. The operational overhead is significant but necessary for production microservices.
Build chaos engineering practices to proactively find failure modes. Deliberately inject failures: kill service instances, add network latency, corrupt responses. Verify that circuit breakers work, retries happen correctly, and the system degrades gracefully. Monoliths need this too, but distributed systems have exponentially more failure modes to test.
Security Considerations
Monolithic applications have a single security boundary. Requests enter through one entry point (API gateway, load balancer), authentication happens once, authorization happens in the application layer. Audit logging tracks all actions in one database. Vulnerability scanning checks one codebase and one set of dependencies.
Microservices create multiple security boundaries. Each service needs authentication, authorization, and audit logging. Inter-service communication requires securing traffic between services — using mutual TLS, API tokens, or service meshes. Each service has its own dependency tree to scan for vulnerabilities. Each deployment pipeline needs security scanning and approval gates.
The attack surface expands with microservices. In a monolith, compromising one component gives access to everything, but there's one component to secure. In microservices, compromising one service might give limited access if services have proper isolation, but there are more services to compromise. Defense in depth matters more when services communicate over networks rather than function calls.
Service-to-Service Authentication
When Service A calls Service B, how does Service B verify the request is legitimate? Monoliths don't have this problem — function calls don't need authentication. Microservices need a service authentication mechanism.
Common approaches include API tokens (each service has a secret token), mutual TLS (services verify each other's certificates), or service mesh authentication (Istio, Linkerd handle authentication transparently). Each adds operational complexity. API tokens need rotation. Mutual TLS needs certificate management. Service meshes need cluster infrastructure.
Implement zero trust networking: assume any network can be compromised and encrypt all traffic between services. Use service identity (which service is calling) separate from user identity (which user initiated the request). Pass user context through requests so Service B can make authorization decisions based on both service and user identity.
Common Failure Patterns to Avoid
The distributed monolith failure pattern emerges when teams split a monolith into microservices but maintain tight coupling. Services call each other synchronously for every operation. Deploying one service requires deploying others. Shared databases across services create hidden dependencies. You've paid the complexity cost of microservices without gaining the benefits.
Symptoms of distributed monoliths include: services that always deploy together, direct database access across service boundaries, synchronous request chains where Service A calls Service B calls Service C for every request, and shared libraries that create tight coupling. Fix this by identifying actual service boundaries based on business capabilities and refactoring to eliminate cross-service database access.
The premature optimization failure happens when teams choose microservices before they need them. You're optimizing for problems you don't have yet — team scaling, differential scaling, deployment independence — while creating problems that definitely exist now: distributed debugging, deployment complexity, network reliability. Start with a monolith. Extract services when you have concrete evidence they're needed.
The Nanoservices Anti-Pattern
Nanoservices are services so small they provide minimal value. A service that only validates email addresses. A service that formats dates. These create network calls for trivial operations that should be library functions. Each nanoservice has its own deployment pipeline, monitoring, and operational overhead for functionality that could be a 10-line function.
Services should be large enough to encapsulate meaningful business capabilities but small enough to be owned by one team. A payment service that handles payment processing, refunds, and payment method management makes sense. Three separate services for these capabilities creates coupling without benefit — refunds always interact with payments.
Apply the two-pizza team rule: if a team can own multiple services, those services might be too small. Conversely, if multiple teams need to coordinate on one service, it might be too large. Service size should match team ownership boundaries, not technical decomposition whims.
Making the Decision for Your Context
Evaluate your specific constraints against the decision framework. If you have fewer than 15 developers, limited DevOps expertise, and uniform scaling patterns, monolithic architecture is the optimal choice. You'll ship features faster and spend less time on infrastructure.
If you have multiple teams that need independent deployment, features with 10x scaling differences, and strong operational maturity, microservices architecture provides measurable benefits. The complexity cost is justified by the organizational and scaling benefits.
For teams in between, build a modular monolith. Get the benefits of clean boundaries and future extraction capability without paying the distributed systems cost. Measure specific pain points: deployment coordination friction, scaling inefficiency, team velocity constraints. When these pains exceed the cost of microservices complexity, extract specific services.
Resist cargo-culting architecture decisions from big tech companies. Netflix uses microservices because they have hundreds of developers and genuinely different scaling patterns. Your 10-person team building a B2B SaaS product has different constraints. Choose the architecture that fits your current reality, not your aspirational future state.
Frequently Asked Questions
Should I start a new project with microservices if I know I'll need them eventually?
No. Start with a well-structured monolith and extract services when you have concrete evidence they're needed. The evidence might be team size hitting 15+ developers, specific features requiring different scaling, or deployment coordination becoming a bottleneck. Until you have specific pain points, microservices create complexity without benefit. Building a modular monolith from the start makes future extraction straightforward when you actually need it.
How many microservices is too many?
The answer depends on team size and operational maturity. A general rule: one team (5-8 developers) can effectively own 2-4 services. More than that and operational overhead dominates development time. If you have 100 services and 10 developers, you're likely suffering from the nanoservices anti-pattern. Look for opportunities to consolidate services that always change together or have tight coupling.
Can I use different databases for different microservices?
Yes, this is one of microservices' key benefits — choosing the optimal database per service. Use PostgreSQL for transactional data, MongoDB for documents, Redis for caching, Elasticsearch for search. The cost is operational complexity: multiple database systems to maintain, monitor, and backup. Only use different databases when the technical benefits clearly outweigh this complexity. Most services work fine with PostgreSQL.
How do I handle transactions across microservices?
Use the Saga pattern: each service performs its local transaction and publishes events that trigger the next step. If later steps fail, earlier steps execute compensation logic to rollback their changes. This creates eventual consistency rather than immediate consistency. Design your business logic to handle this — for example, reserving inventory before payment rather than relying on atomic transactions. Accept that distributed transactions are fundamentally harder than database transactions.
What's the performance impact of microservices?
Microservices add network latency to operations that span services. Each inter-service call adds 20-50ms of latency compared to in-process function calls. For operations that need data from 5 services, that's 100-250ms of additional latency. This matters for user-facing operations where every 100ms affects conversion rates. Mitigate with caching, async operations where possible, and careful service boundary design to minimize hops on critical paths.
Do microservices save infrastructure costs?
Sometimes. Microservices save costs when differential scaling reduces overprovisioning — running high-traffic services on many servers and low-traffic services on few servers. But they add fixed costs per service (load balancers, databases, monitoring). Calculate the total cost including operational overhead. Many teams find microservices cost more in absolute terms but provide benefits (team velocity, deployment independence) that justify the cost.
How do I do local development with 10+ microservices?
Use Docker Compose to run all services locally, but this is slow and resource-intensive. Better: run only the services you're actively developing locally and point to shared development environments for other services. Use feature flags and mocking to develop features without running the entire system. Some teams build "slim" versions of services that return mock data for local development.
Should I build a monolith first then migrate to microservices?
Yes, for most teams. This lets you discover actual service boundaries based on real usage patterns rather than guessing up front. The exception: if you're certain about boundaries (you're rebuilding an existing system with known requirements) and have strong DevOps expertise, starting with microservices can work. But most teams underestimate distributed system complexity and overestimate their knowledge of the right boundaries.
What's the biggest risk of choosing wrong?
Choosing microservices too early wastes 6-12 months building infrastructure before delivering user value. Teams spend time on service mesh configuration, distributed tracing, and deployment pipelines instead of validating product-market fit. Choosing monolith too late means a costly migration when you hit scaling walls. The risk is asymmetric: monolith-too-early is rarely a fatal mistake, microservices-too-early often is for startups.
Conclusion
The microservices versus monolith decision depends on specific, measurable factors: team size, scaling patterns, deployment independence needs, and operational maturity. Teams with fewer than 15 developers and uniform scaling patterns should default to well-structured monoliths. Teams with multiple independent teams, differential scaling needs, and strong DevOps capabilities benefit from microservices architecture. The modular monolith provides a middle path that maintains extraction optionality.
Make the decision based on current constraints, not future aspirations. Measure the actual costs including infrastructure, operational overhead, and developer time. Choose the architecture that lets your team ship features fastest within your reliability and scale requirements. Recognize that architecture evolves — the right choice today might not be the right choice in two years, and that's fine as long as you've built the flexibility to change.