Skip to content

Microservices Architecture

Introduction

Before we can dive into .NET Aspire it’s important we review Microservices and Cloude Native Architecture.

Cloud Native Computing

A definition of Cloud Native Computing from CNCF:

Cloud native practices empower organizations to develop, build, and deploy workloads in computing environments (public, private, hybrid cloud) to meet their organizational needs at scale in a programmatic and repeatable manner. It is characterized by loosely coupled systems that interoperate in a manner that is secure, resilient, manageable, sustainable, and observable. Cloud native technologies and architectures typically consist of some combination of containers, service meshes, multi-tenancy, microservices, immutable infrastructure, serverless, and declarative APIs — this list is non-exhaustive.

The 5 pillars of cloud native computing are (according to Google):

  1. Microservices: Applications are broken down into smaller, loosely coupled services.
  2. Containers & Orchestration: Containers are used to package and deploy applications, and orchestration tools are used to manage containerized applications.
  3. DevOps: Development and operations teams work together to deliver software faster and more reliably.
  4. Continuous Integration & Continuous Delivery: Automating the process of testing and deploying code, helping teams deliver software faster and more reliably.

Cloud native computing offers many advantages, a few are listed below:

  • Scalability: Cloud native applications can scale up or down based on demand.
  • Resilience: Cloud native applications are designed to be resilient to failures.
  • Agility: Cloud native applications can be developed, tested, and deployed quickly.

But with these advantages come added complexity, as cloud native applications grow, managing them can become more difficult. Developers need also rethink the way we build applications, and convincing different teams to make the move can be difficult. This is where tools like .NET Aspire can help.

What are Microservices

mind blown gif

A simple definition of microservices: an application that is comprised of other smaller applications (services).

A more accurate defintion: Microservices are an architectural style where an application is structured as a collection of independently deployable, loosely coupled services. These services are organized around business capabilities, own their data, and are completely indepedent of each other.

microservices

Organizing Services

Figuring out how to split up your microservices can be difficult. A good rule of thumb is to organize your services around business capabilities or logical operations. Domain Driven Design (DDD) is a good approach to first focus on your business domain and then split your services based on that. Applying the SRP (Single Responsibility Principle) to your services can also help you split them up.

Ownership of data

One of the key principles of microservices is that each service owns its own data. This means that services can easily be scaled independently.

Independence

The services in a microservices architecture are completely indepedent of each other. Depedending on the size of the application and organization you may have several teams working on different services.

Services could be written with different tech stacks, programming languages, and even hosted on different platforms, though you probably don’t want to go too extreme with this. Sticking to a few programming languages and tech stacks depdending on the organization is a good idea.

Services can also be setup in your CI/CD pipeline to deplopy independently, allowing teams to deploy their services quickly without affecting other services.

Event Driven Architecture (EDA)

Even though our services are indepdent of each other we still need a way for them to interact with each other. While RESTful APIs are a common way to communicate, using message brokers like Kafka, RabbitMQ, Azure Service Bus can be a better approach.

If you are familiar with the mediator pattern, this is similar in the way it reduces coupling by having an intermediary handle the communication. The microservices do not “know” about each other, they simply must have the ability to react to messages that are sent from the message broker.

Advantages of using EDA include:

  • Decoupling between services
  • Asynchronous communication - non blocking calls from producers and consumers.

Messages that are sent between services are usually:

  • Commands: A message that tells a service to do something.
  • Events: A message that tells a service that something has happened.
  • Can contain data (JSON string, bytes, etc) or be empty.

Events can further be broken down into:

  • Domain events: Events that are specific to a domain.
  • Integration events: Events that are used to communicate between services.
  • There are other types of events but these are the most common.

In EDA the message broker is a single point of failure and can become the bottleneck of your application. This should be taken into considertation and have a high amount of resilience in place. If you are using a cloud provider like Azure, AWS, or GCP they have services that can help mitigate these risks by offering managed message broker services with built-in resilience, high availability, and fault tolerance. For example, Azure offers Azure Service Bus, AWS provides Amazon SQS and Amazon SNS, and GCP has Pub/Sub. These services are designed to handle large-scale workloads, ensure message durability, and automatically recover from failures.

Overview of the Publish-Subscribe Pattern

There are many ways to implement EDA, but one of the most common implementations is the Publish-Subscribe pattern. Publishers send messages to a message broker and subscribers consume these messages.

pub sub

This pattern allows for multiple subscribers to listen to the same messages and react to them accordingly. Additional filtering can be applied to reduce the number of messages a subscriber receives. This is an implementation detail we will go over in a later section. For now understanding that messages are sent to a message broker, and distributed to subscribers (services) is how we can communicate in a loosely coupled manner.

Pictured below is a use case when a new user regsiters for our application there might be several actions that need to take place. The services can handle that same event independently in this case sending an email, setting up the users default todo reminders and a default user goal.

pub sub

Event Sourcing Pattern

An advantage of using EDA is that you can implement the Event Sourcing Pattern to have a log of events that have happened in your system. This is adventageous for many reasons, including:

  • A single source of truth: You can use the events to rebuild the state of your system at any point in time.
  • Debugging: Of course being able to move back in time to see exactly what happened is a great way to debug issues.
  • Auditing: You can see who did what and when.

There are some caveats to using Event Sourcing, including the complexity and something called eventual consistency. Eventual consistency means that data will be consistent at some point in the future, but not outright. The delay in consistency can be milliseconds to seconds, of course the goal being to minimize this delay.

The Death Star

If you decide to not use EDA and have services communicate directlly with each other you run the risk of creating a tightly coupled interdependent system. This is known as the Death Star anit-pattern.

It might be okay for smaller projects (technically the example application does this) but as your application grows to 100s of microservices you can create a mess of dependencies, which are hard to manage, deploy, and scale effictively removing the advantages of using a microservices architecture.

Pictured below is an example which would be hard to untagle! Just like a drawer of unmanaged cables, but much worse.

pub sub

The Gateway Patterns

Gateways can provide a single entry for your frontend to interface with simplifying the communication between your backend services. These gateways can also handle authentication, logging, caching, and other cross-cutting concerns. This allows aggregating responses and translating one external request into many internal ones, even the ability to translate a request into different protocols.

For the sample application we will not be using a gateway, I have configured the vite dev server to proxy requests to the appropriate services, and the nginx docker file to do the same.

Similar to the message broker, the gateway can become a bottle neck, and single point of failure so having a high amount of resilience is important.

There are multiple ways to implement a gateway, these patterns can be mixed and matched to fit your needs they include:

  • Gateway Routing Pattern: A gateway that routes requests by the frontend directly to internal services, this is what most people think of when they hear gateway.
  • Gateway Aggregation Pattern: A gateway that aggregates responses from multiple services into a single response.
  • Backend for Frontend Pattern: A gateway that is specific to a frontend, this is useful when you have multiple frontends that require different data or capabilities.
  • Gateway Offloading Pattern: A gateway that offloads cross-cutting concerns like authentication, logging, and caching from the services.

Cloud gateways that are currently available include: Azure Application Gateway, AWS API Gateway, and GCP API Gateway. If you want to deploy and fine grain control of your gateway you can use libraries like:

  • Ocelot: A open-source .NET API gateway which includes support for routing, request aggregation, and authentication, load balancing, and more.
  • YARP: Developers love their acronyms, YARP stands for Yet Another Reverse Proxy (similar to YAML in naming). The library was developed by Microsoft and is a reverse proxy that can accomplish many of the same tasks as Ocelot.
  • Envoy: A high performance C++ distributed proxy designed for single services and applications, as well as a communication bus and “universal data plane” designed for large microservice “service mesh” architectures.