Microservices

Part 1, Chapter 3


A microservice architecture provides a means of breaking apart large, monolithic applications into smaller services that interact and communicate with each another. Each service has independent deliverables, so each one can be deployed, upgraded, scaled, and replaced on their own, separate from the whole. Communication between the services usually happens over a network connection through HTTP calls (request/response). Web sockets, message queues, pub/sub, and remote procedure calls (RPC) can also be used to connect standalone components.

With monolithic applications, every piece of business logic resides in a single application. The goal of micoservices is to break up that logic into separate services.

Each individual service focuses on a single task, generally separated by business unit or domain, and is often governed by its RESTful contract. Well-designed services have clear inputs and outputs which provides clarity and helps eliminate unforeseen side effects.

The goal of this course is to detail one approach to developing an application in the microservice fashion. It's less about the why and more about the how. Microservices are hard. They present a number of challenges and issues that are very difficult to solve. Communication and soft skills are key. Keep this in mind before you start breaking apart your monolith.

Pros

Separation of Concerns

With a clear separation between services, developers are free to focus on their own areas of expertise, like languages, frameworks, dependencies, tools, and build pipelines.

For example, a front-end JavaScript developer could develop the client-facing views without ever having to understand the underlying code in the back-end API. He or she is free to use the languages and frameworks of choice, only having to communicate with the back-end via AJAX requests to consume the RESTful API. Put another way, developers can treat a service like a black box since services communicate via APIs. The actual implementation and complexity are hidden.

That said, it's a good idea to create some organization-wide standards to help ensure each team can work and function together -- like code quality and style checking, code reviews, API design.

Clear separation means that errors are mostly localized to the service that the developer is working on. This increases the resiliency of the system by preventing total system failures when a single service fails. So, you can assign a junior developer to a less critical service so that way if they bring down that service, the remainder of the application is not affected.

Loosely coupled services are easier to scale since each service can be deployed separately. Higher demand services often have different scaling requirements. For example, perhaps a particular process requires a high GPU processor and RAM. Well, the service that houses that process can be deployed on a server that meets those requirements while the other services can run on normal servers. Less coupling also helps to eliminate one team having to wait on another team to finish up work that another team may be dependent on.

Smaller Code Bases

Smaller code bases tend to be easier to understand since you do not have to grasp the entire system. This, along with the necessity for solid API design, means that applications in a microservice stack are generally faster to develop and easier to test, refactor, and scale (if they're responsible for one thing). They're less brittle as well since applications are decoupled from one another. Just keep in mind that it's important to maintain consistent coding standards across all services, so that it's easier for a developer to move from one service to another.

Accelerated Feedback Loops

With microservices, developers often own the entire lifecycle of the app, from inception to delivery. Instead of aligning teams with a particular set of technologies -- like client UI, server-side, QA, etc. -- teams are more product-focused, responsible for delivering the application to the customers themselves. Because of this, they have much more visibility into how the application is being used in the real-world. This speeds up the feedback loop, making it easier to fix bugs and iterate.

Cons

Design Complexity

Deciding to split off a piece of your application into a microservice is no easy task. It's often much easier to refactor it into a separate module within the overall monolith rather than splitting it out.

Once you split out a service there is no going back.

Network Complexity

With a monolith, generally everything happens in a single process so you don't have to make very many calls to other services. As you break out pieces of your application into microservices, you'll find that you'll now have to make a network call when before you could just call a function.

This can cause problems especially if multiple services need to communicate with one another, resulting in ping-pong-like effect in terms of network requests. You will also have to account for a service going down altogether.

Infrastructure

With multiple services, complexity shifts from the codebase to the platform and infrastructure. This can be costly. Plus, you have to have the right tools and human resources in place to manage it.

Data Persistence

Most applications have some sort of stateful layer, like databases or task queues. Microservice stacks also need to keep track of where services are deployed and the total number of deployed instances, so that when a new instance of a particular service is stood up, traffic can be re-routed appropriately. This is often referred to as service discovery.

When dealing with containers, you'll need to take special care into how you handle the data volumes associated with stateful services since they need to persist beyond the life of a single container.

Isolating a particular service's state so that it is not shared or duplicated is incredibly difficult. You'll often have to deal with various sources of truth, which will have to be reconciled frequently. Again, this comes down to design.

Data Management

Managing data in a microservices architecture is particularly challenging.

Patterns:

  1. Database per service
  2. Shared database
  3. API Composition
  4. Command Query Responsibility Segregation (CQRS)
  5. Event sourcing

From a coupling standpoint, it's best to have each service manage it's own data. However, this can result in high network overhead if you have lots of transactions that need to hit multiple services.

Integration Tests

Often, when developing applications with a microservice architecture, you cannot fully test out all services until you deploy to a staging or production server. This takes much too long to get feedback. Fortunately, Docker helps to speed up this process by making it easier to link together small, independent services locally.

Logging, monitoring, and debugging are much more difficult as well.

For more on testing a microservice, review the Testing Strategies in a Microservice Architecture guide.

Additional Resources

Microservices work best when you can split your application into clear services that:

  1. Can be deployed independently
  2. Only communicate with each other asynchronously
  3. Master their own data

For more, review these excellent resources:

  1. 35 Microservices Interview Questions You Most Likely Can't Answer
  2. One less microservice than you need
  3. Microservices Design Guide



Mark as Completed