Microservices architecture is the approach to building software applications as a collection of small, independently deployable services — each responsible for a specific business capability, each running in its own process, and each communicating with other services through well-defined APIs. The microservices approach contrasts with the monolithic architecture in which all application functionality is implemented in a single deployable unit — a single codebase, compiled and deployed together, sharing the same runtime and database.
The specific organisational and operational problems that microservices most effectively address: the deployment coupling problem (in a monolith, deploying a change to the payment service requires deploying the entire application, creating risk for the user service, the catalogue service, and every other service in the system), the scaling constraint (the monolith must be scaled as a whole, even when only one component has high load), the technology lock-in (the monolith must be implemented in a single technology stack, while microservices allow each service to use the technology best suited to its specific requirements), and the team autonomy limitation (multiple teams working in the same monolithic codebase compete for the shared resource and must coordinate changes that might affect each other’s code).
Microservices Design Principles
The microservice design principles that most reliably produce services that are genuinely independent rather than distributed monolith components: the single responsibility principle applied at the service level (each service should be responsible for a specific, bounded business capability that can be described clearly — the payment service processes payments, the notification service sends notifications, the inventory service tracks stock levels), the database independence principle (each service owns its own data store and other services access its data through the service’s API rather than through shared database access), and the failure independence principle (each service should be designed so that its failure does not cascade into the failure of services that depend on it — through circuit breakers, timeouts, and graceful degradation).
The microservice boundary design approach that most consistently produces well-designed services: the Domain-Driven Design (DDD) concept of bounded contexts, which identifies the natural boundaries within the business domain where each context has its own language (the word customer means different things to the order management context and the customer support context), its own data model, and its own business logic. Services designed around bounded contexts represent coherent, business-meaningful units of functionality rather than arbitrary technical divisions that create the artificial dependencies that make distributed systems harder rather than easier to manage.
The Distributed Systems Complexity Cost
The complexity problems that microservices introduce that the monolith does not have: the network communication between services that replaces in-process function calls (introducing latency, the possibility of network failure, and the need for serialisation and deserialisation of data that in-process calls do not require), the distributed data consistency challenge (the monolith that can update multiple database tables in a single transaction cannot be replicated in microservices where each service has its own database, requiring eventual consistency patterns that are significantly harder to reason about than transactional consistency), and the operational complexity increase (deploying, monitoring, and debugging a system of twenty services is significantly more complex than deploying, monitoring, and debugging a single application).
The microservices complexity manifestation that most catches teams off guard when they first adopt the architecture: the distributed tracing requirement. The debugging session that finds the source of an error in a monolith by examining a single log file or stack trace becomes, in a microservices system, the investigation of which of the twenty services involved in the request chain introduced the error — a challenge that requires distributed tracing infrastructure (Jaeger, Zipkin, or cloud-native equivalents) to trace a single request’s path through all the services it invoked. The operational maturity required to effectively manage a microservices system significantly exceeds what most teams anticipate when they begin the migration from a monolith.
When to Use and When to Avoid Microservices
The organisational conditions that most strongly indicate microservices is the right architectural choice: the large development organisation (the microservices architecture provides the team autonomy benefit most clearly when multiple large teams are working on the same system and the deployment coupling and shared codebase of a monolith create significant coordination overhead), the truly independent scaling requirements (the system component that receives one thousand times the traffic of the rest of the system genuinely benefits from the ability to scale independently rather than scaling the entire application to accommodate one component’s load), and the diverse technology requirements (the system where different components genuinely benefit from different technologies — a machine learning component that needs Python, a high-throughput data processing component that benefits from a JVM language, and a user interface component built in JavaScript — can use each technology in a separate service).
The most reliable indicator that a team should remain with a monolith rather than migrating to microservices: the small team size. The microservices architecture’s primary value is team autonomy — enabling multiple teams to work independently without coordination overhead. The team of five or ten engineers working on a single product does not have the coordination overhead that microservices address and gains primarily the operational complexity cost that microservices impose. The widely cited rule of thumb — that a service should be manageable by a team that can be fed by two pizzas — suggests that the organisational scale where microservices provide their primary benefit requires at minimum the scale where multiple pizza-sized teams are working on the same system.
Migrating From Monolith to Microservices
The monolith-to-microservices migration approach that most reliably produces working services without the big-bang rewrite that attempts to replace the entire monolith at once: the strangler fig pattern, in which new functionality is implemented as separate services from the beginning, and existing monolith functionality is incrementally extracted into services as the team accumulates experience with the microservices infrastructure. The strangler fig migration maintains the monolith as the primary system while gradually replacing its components — the new payment processing logic is implemented as a payment service while the old payment code in the monolith continues to function, until the service is ready to handle all payment requests and the monolith code can be retired.
The migration sequencing principle that most reduces the risk of the strangler fig migration: extracting the service that is most clearly bounded, most clearly independent of the monolith’s internal state, and most clearly valuable to deploy independently — rather than starting with the most technically interesting extraction or the most complex business logic. The notification service that sends emails and messages, the file conversion service that processes uploaded documents, and the analytics service that aggregates usage data are examples of components that are naturally bounded, clearly separable, and independently deployable — making them better candidates for early extraction than the core order processing or user management logic that is deeply entangled with the rest of the monolith.
