In software engineering, a microservice architecture is an architectural pattern that organizes an application into a collection of loosely coupled, fine-grained services that communicate through lightweight protocols. This pattern is characterized by the ability to develop and deploy services independently, improving modularity, scalability, and adaptability. However, it introduces additional complexity, particularly in managing distributed systems and inter-service communication, making the initial implementation more challenging compared to a monolithic architecture.
There is no single, universally agreed-upon definition of microservices. However, they are generally characterized by a focus on modularity, with each service designed around a specific business capability. These services are loosely coupled, independently deployable, and often developed and scaled separately, enabling greater flexibility and agility in managing complex systems. Microservices architecture is closely associated with principles such as domain-driven design, decentralization of data and governance, and the flexibility to use different technologies for individual services to best meet their requirements.
It is common for microservices architectures to be adopted for cloud-native applications, serverless computing, and applications using lightweight container deployment via OS-level virtualization. According to Fowler, because of the large number (when compared to monolithic application implementations) of services, decentralized continuous delivery and DevOps with holistic service monitoring are necessary to effectively develop, maintain, and operate such applications. A consequence of (and rationale for) following this approach is that the individual microservices can be individually scaled. In the monolithic approach, an application supporting three functions would have to be scaled in its entirety even if only one of these functions had a resource constraint. With microservices, only the microservice supporting the function with resource constraints needs to be scaled out, thus providing resource and cost optimization benefits.
Cell-based architecture is a distributed computing design in which computational resources are organized into self-contained units called cells. Each cell operates independently, handling a subset of requests while maintaining scalability, fault isolation, and availability.
A cell typically consists of multiple microservices and functions as an autonomous unit. In some implementations, entire sets of microservices are replicated across multiple cells, enabling requests to be rerouted to another operational cell if one experiences a failure. This approach is intended to improve system-wide resilience by limiting the impact of localized failures.
Some implementations incorporate circuit breakers within and between cells. Within a cell, circuit breakers may be used to mitigate cascading failures among microservices, while inter-cell circuit breakers can isolate failing cells and redirect traffic to those that remain operational.
Cell-based architecture has been adopted in certain large-scale distributed systems where fault isolation and redundancy are design priorities. Its implementation varies based on system requirements, infrastructure constraints, and specific operational goals.
In 1999, software developer Peter Rodgers had been working on the Dexter research project at Hewlett Packard Labs, whose aim was to make code less brittle and to make large-scale, complex software systems robust to change. Ultimately this path of research led to the development of resource-oriented computing (ROC), a generalized computation abstraction in which REST is a special subset. In 2005, during a presentation at the Web Services Edge conference, Rodgers argued for "REST-services" and stated that "Software components are Micro-Web-Services... Micro-Services are composed using Unix-like pipelines (the Web meets Unix = true loose-coupling). Services can call services (+multiple language run-times). Complex service assemblies are abstracted behind simple URI interfaces. Any service, at any granularity, can be exposed." He described how a well-designed microservices platform "applies the underlying architectural principles of the Web and REST services together with Unix-like scheduling and pipelines to provide radical flexibility and improved simplicity in service-oriented architectures.
Also in 2005, Alistair Cockburn wrote about hexagonal architecture which is a software design pattern that is used along with the microservices. This pattern makes the design of the microservice possible since it isolates in layers the business logic from the auxiliary services needed in order to deploy and run the microservice completely independent from others.
Determining the appropriate level of (micro)service granularity in a microservices architecture often requires iterative collaboration between architects and developers. This process involves evaluating user requirements, service responsibilities, and architectural characteristics, such as non-functional requirements. Neal Ford highlights the role of integrator and disintegrator factors in this context. Integrator factors, such as shared transactions or tightly coupled processes, favor combining services, while disintegrator factors, such as fault tolerance or independent scalability, encourage splitting services to meet operational and architectural goals. Additionally, fitness functions, as proposed by Neal Ford, can be used to validate architectural decisions and service granularity by continuously measuring system qualities or behaviors that are critical to stakeholders, ensuring alignment with overall architectural objectives.
In microservices architectures, service granularity influences testing, deployment, performance, and reliability. Very fine-grained microservices are typically easier to test and deploy independently, but they often experience lower performance and reduced overall reliability due to increased interservice communication and more complex service choreography. Coarse-grained services exhibit contrasting characteristics. They generally provide higher robustness and reliability by minimizing communication overhead and coordination complexity, but they are more challenging to test and deploy because modifications affect a broader functional scope. The appropriate level of service granularity is determined by business drivers. Architectural decisions commonly begin with identifying these drivers and then aligning architectural characteristics such as performance, scalability, reliability, or deployment flexibility to support them.
A bounded context, a fundamental concept in domain-driven design (DDD), defines a specific area within which a domain model is consistent and valid, ensuring clarity and separation of concerns. In microservices architecture, a bounded context often maps to a microservice, but this relationship can vary depending on the design approach. A one-to-one relationship, where each bounded context is implemented as a single microservice, is typically ideal as it maintains clear boundaries, reduces coupling, and enables independent deployment and scaling. However, other mappings may also be appropriate: a one-to-many relationship can arise when a bounded context is divided into multiple microservices to address varying scalability or other operational needs, while a many-to-one relationship may consolidate multiple bounded contexts into a single microservice for simplicity or to minimize operational overhead. The choice of relationship should balance the principles of DDD with the system's business goals, technical constraints, and operational requirements.
The benefit of decomposing an application into different smaller services are numerous:
The microservices approach is subject to criticism for a number of issues:
The architecture introduces additional complexity and new problems to deal with, such as latency, message format design, backup/availability/consistency (BAC), load balancing and fault tolerance. All of these problems have to be addressed at scale. The complexity of a monolithic application does not disappear if it is re-implemented as a set of microservices. Some of the complexity gets translated into operational complexity. Other places where the complexity manifests itself are increased network traffic and slower performance. Also, an application made up of any number of microservices has a larger number of interface points to access its respective ecosystem, which increases the architectural complexity. Various organizing principles (such as hypermedia as the engine of application state (HATEOAS), interface and data model documentation captured via Swagger, etc.) have been applied to reduce the impact of such additional complexity.
Microservices are susceptible to the fallacies of distributed computing â a series of misconceptions that can lead to significant issues in software development and deployment.
Ideally, microservices follow a "share-nothing" architecture. However, in practice, microservices architectures often encounter situations where code must be shared across services. Common approaches to addressing this challenge include utilizing separate shared libraries for reusable components (e.g., a security library), replicating stable modules with minimal changes across services, or, in certain cases, consolidating multiple microservices into a single service to reduce complexity. Each approach has its advantages and trade-offs, depending on the specific context and requirements.
Richards & Ford in Fundamentals of software architecture (2020) propose each microservice should have its own architectural characteristics (a.k.a. non functional requirements), and architects should not define uniform characteristics for the entire distributed system.
To avoid having to coordinate deployments across different microservices, Sam Newman suggests keeping the interfaces of microservices stable and making backwards-compatible changes as interfaces evolve. On the topic of testing, Newman in Building Microservices (2015) proposes consumer-driven contract testing as a better alternative to traditional end-to-end testing in the context of microservices. He also suggests the use of log aggregation and metrics aggregation as well as distributed tracing tools to ensure the observability of systems composed of microservices.
Computer microservices can be implemented in different programming languages and might use different infrastructures. Therefore, the most important technology choices are the way microservices communicate with each other (synchronous, asynchronous, UI integration) and the protocols used for the communication (e.g. RESTful HTTP, messaging, GraphQL). In a traditional system, most technology choices like the programming language impact the whole system. Therefore, the approach to choosing technologies is quite different.
The Eclipse Foundation has published a specification for developing microservices, Eclipse MicroProfile.
In a service mesh, each service instance is paired with an instance of a reverse proxy server, called a service proxy, sidecar proxy, or sidecar. The service instance and sidecar proxy share a container, and the containers are managed by a container orchestration tool such as Kubernetes, Docker Swarm, or DC/OS. The service proxies are responsible for communication with other service instances and can support capabilities such as service (instance) discovery, load balancing, authentication and authorization, secure communications, and others.