- are an approach to distributed systems that promote the use of finely gained services with their own life cycles, which collaborate together.
- are primarily modeled around business domains, they avoid the problems of traditional tiered architectures
- integrate new technologies and techniques that have emerged over the last decade, which helps us avoid the pit-falls of many service-oriented architecture implementations
Concrete examples are present successfully around the world, in organizations like Amazon, Netflix and more,who have found that the increased autonomy of this architecture gives their teams, is a huge advantage.
Principles of Microservices
- are statements about how things should be done, and why we think they should be done that way, and help us frame the various decisions we have to make when building our platform
1. Model Around Business Concepts
History has shown us that interfaces structured around business-bounded contexts are more stable than those structured around technical concepts.By modeling the domain in which our system operates, not only do we attempt to form more stable interfaces, but we also ensure that we are better able to reflect changes in business processes easily.
2. Adopt a Culture of Automation
Microservices add a lot of complexity, a key part of which comes from the sheer number of moving parts we have to deal with. Embracing a culture of automation is one key way to address this, and front-loading effort to create the tooling to support microservices can make a lot of sense.
Automated testing is essential, as ensuring our services still work is a more complex process than with monolithic systems. Having a uniform command-line call to deploy the same way everywhere can help, and this can be a key part of adopting continuous delivery to give us fast feedback on the production quality of each check-in.
Using environment definitions help us specify the differences from one environment to another, without sacrificing the ability to use a uniform deployment method. Creating custom images help us speed up deployment and embrace the creation of fully automated immutable servers to make it easier reason about our system.
3. Hide Internal Implementation Details
To maximize the ability of one services to evolve independently of any others, it is vital that we hide implementation details. Modeling bounded contexts can help, as this helps us focus on those models that should be shared, and those that should be hidden.
Services should also hide their databases to avoid falling into one of the most common sorts of coupling that can appear in traditional service-oriented architectures, and use data pumps or event data pumps to consolidate data across multiple services for reporting purposes.
Where possible, we have to pick technology-agnostic APIs to give us the freedom to use different technology stacks. Using REST will help us formalize the separation of internal and external implementation details, although we are open to other architectural styles that will embrace these ideas.
4. Decentralize All the Things
To maximize the autonomy that microservices make possible, we need to constantly be looking for the chance to delegate decision making and control to the teams that own the services themselves. This process starts with embracing self-service wherever possible, allowing people to deploy software on demand, making development and testing as easy as possible, and avoiding the need for separate teams to perform these activities.
- Ensuring that teams own their services is an important step on this journey, making teams responsible for the changes that are made, ideally even having them decide when to release those changes.
- Making use of internal open source ensures that people can make changes on services owned by other teams, although this requires work to implement.
- Align teams to the organization to ensure that Conway’s law works for us, and help our team become domain experts in the business-focused services they are creating.
- Where some overarching guidance is needed, we can embrace a shared governance model where people from each team collectively share responsibility for evolving the technical vision of the system.
This principle can apply to architecture too, as it follows:
- Avoid approaches like enterprise service bus or orchestration systems, which can lead to centralization of business logic and dumb services.
- Instead, prefer choreography over orchestration and dumb middleware, with smart endpoints to ensure that we keep associated logic and data within service boundaries, helping keep things cohesive.
5. Independently Deployable
We should always strive to ensure that our microservices can and are deployed by themselves. Even when breaking changes are required, we should seek to coexist versioned endpoints to allow our consumers to change over time.
This allows us to optimize for speed of release of new features, as well as increasing the autonomy of the teams owning these microservices by ensuring that they don’t have to constantly orchestrate their deployments.
By adopting a one-services-per-host model, you reduce side effects that could cause deploying one services to impact another unrelated service.We have to consider using blue/green or canary release techniques to separate deployment from release, reducing the risk of a release going wrong; using consumer-driven contracts can help us catch breaking changes before they happen.
We have to remember that it should be the norm, not the exception, that we can make a change to a single service and release it into production, without having to deploy any other services in lock-step.
Our consumers should decide when they update themselves, and we need to accommodate this.
6. Isolate Failure
A microservice architecture can be more resilient than a monolithic system, but only if we understand and plan for failures in part of our platform. If we don’t account for the fact that a downstream call can and will fail, our platform might suffer catastrophic cascading failure, and we could find ourselves with a system that is much more fragile than before.
When using network calls, don’t treat remote calls like local calls, as this will hide different sorts of failure mode; so we have to make sure if we’re using client libraries, that the abstraction of the remote call doesn’t go too far.
If we hold the tenets of antifragility in mind, and expect failure will occur anywhere and everywhere, we are on the right track. We have to make sure that our timeouts are set appropriately and understand when and how to use bulkheads and circuit breakers to limit the fallout of a failing component.
We have to understand what the customer-facing impact will be if only one part of the system is misbehaving, and know what the implications of a network partition might be, and whether sacrificing availability or consistency in a given situation is the right call.
7. Highly observable
We cannot rely on observing the behavior of a single service instance or the status of a single machine to see if the system is functioning correctly. Instead, we need a joined-up view of what is happening.
We have to use semantic monitoring to see if our platform is behaving correctly, by injecting synthetic transactions into our system to simulate real-user behavior.
We must aggregate our logs, and aggregate our stats, so that when we see a problem we can drill down to the source; when it comes to reproducing nasty issues or just seeing how our system is interacting in production, we have to use correlation IDs to allow us to trace calls through the system.
As you have finished reading the high-level overview of the microservices architecture, make sure to follow us for the next follow-up article with focus on specifics regarding our platform, as this is a journey and not a destination.
Quotes, credits, references:
Subscribe to GoHelpFund Blog
Get the latest posts delivered right to your inbox