SSW Foursquare

Rules to Better Microservices - 6 Rules

A microservice architecture is an application architecture where an application consists of many loosely-coupled services. The communications between services are kept lightweight, and the API interfaces between them need to be carefully managed. They are designed to allow different teams to work on different parts of the application completely independently.

  1. Microservices - Do you break down your apps?

    There are two common types of application architecture:

    • Monoliths (aka N-Tier applications)
    • Microservices

    Monoliths have their place. They are easy to get going and often make a lot of sense when you are starting out. However, sometimes your app may grow in size and become difficult to maintain. Then you might want to consider Microservices...

    Microservices let you break down your app into little pieces to make them more manageable, replaceable and maintainable. You can also scale out different parts of your app at a granular level.

    .NET 6 and Azure have heaps of great tools for developing simple APIs and worker services in a Microservices pattern.

    Watch the below video from 35:40 - 46:50

    The tools of the trade

    • .NET Worker Services make it easier to implement dependency injection, configuration and other syntactic sugar using the same patterns you are familiar with in other types of .NET applications
    • Azure Container Apps give you a way to host different little subsections of the application
    • Azure Functions gives you a great way to build applications in small, modular, scalable and easy to manage chunks. It provides triggers other than http to handle other common microservice patterns
    • Minimal APIs give you a way to write APIs in just a few short lines of code

    What's the point?

    • Cost - Provides separation of scalability, keep the hot parts of your app hot and the cold parts of your app cold to achieve maximum pricing efficiency
    • Maintainability - Keep code more manageable by making it bite sized chunks
    • Simplify code - Write minimal APIs
    • Deployment - Standardize deployment with containers
    • Testing - Easier to find problems since they are isolated to a specific part of the app
    • Cognitive Complexity - Devs can focus on one aspect of the app at a time
    • Data - You can use the best way of storing data for each service
    • Language - You can use the best language for each service

    What's the downside?

    • Upfront Cost - More upfront work is required
    • Cognitive Complexity - While individual apps are simpler, the architecture of the app can become more complex
    • Health Check - It's harder to know if all parts are alive
    • Domain boundaries - You need to define the separation of concerns between different services. Avoid adding dependencies between services because you can create a domino of failures...a house of cards.
    • Performance normally suffers as calls are made between services
    • Without adequate testing it's harder to maintain
    • Using multiple languages and datastores can be both more expensive to host and require more developers

    What new techniques are required

    • Contract Testing - To mitigate the risk of changes in one service breaking another, comprehensive tests that check the behaviour of services is required
  2. Do you know the key components of a Microservice solution?

    Microservice architectures consist of a number of components

    These often include:

    • An API Gateway (think APIM, Ocelot, YARP, Azure Front Door)
    • Support different types of frontends: Web, Desktop, Mobile
    • Flexible deployment model in subsequent microservices
    • Each microservice is in charge of its own data store
    • Event driven
    • VNet integration
    • Messaging system (used to decouple services, think Azure SendGrid or ServiceBus)
  3. Do you use MassTransit to build reliable distributed applications?

    When building distributed applications messaging is a common pattern to use. Often we might take a hard dependency on a specific messaging technology, such as Azure Service Bus or RabbitMQ. This can make it difficult to change messaging technologies in the future. Good architecture is about making decisions that make things easy to change in future. This is where MassTransit comes in.

    MassTransit is a popular open-source .NET library that makes it easy to build distributed applications using messaging without tying you to one specific messaging technology.

    .NET Messaging Libraries

    There are several .NET messaging libraries that all abstract the underlying transport. These include:

    There are also the service bus specific libraries:

    Advantages of using MassTransit

    ✅ Open-source and free to use

    ✅ Enables swapping of messaging transports by providing a common abstraction layer

    ✅ Supports multiple messaging concepts:

    • Point-to-Point
    • Publish/Subscribe
    • Request/Response

    ✅ Supports multiple messaging transports:

    • In-Memory
    • RabbitMQ
    • Azure Service Bus
    • Amazon SQS
    • ActiveMQ
    • Kafka
    • gRPC
    • SQL/DB

    ✅ Supports complex messaging patterns such as Sagas

    Scenarios

    Scenario 1 - Modular Monolith

    A Modular Monolith architecture requires all modules to be running in a single process. MassTransit can be used to facilitate in-memory communication between modules in the same process.

    This allows us to send events between modules and also request data from other modules.

    Scenario 2 - Azure Hosted Microservices

    When building microservices in Azure, it's common to use Azure Service Bus as the messaging transport. With minimal changes, MassTransit can be used to send messages to and from Azure Service Bus instead of using the In-Memory transport.

    Scenario 3 - Locally Developing Microservices

    When developing microservices locally, it's common to use containers for each service. However, some of the cloud based messaging services (e.g. Azure Service Bus) are not able to be run in a container locally. In this scenario, we can easily switch from using the Azure Service Bus transport to Containerized RabbitMQ transport

    Demo Code

    If you're interested in seeing MassTransit in action, check out github.com/danielmackay/dotnet-mass-transit

  4. Do you know how to choose the best software architecture for your system?

    Choosing the right software architecture for your system is crucial for its success and maintainability. Making the wrong choice can lead to increased complexity, difficulty in scaling, and higher costs.

    Here are some of the popular architectures and factors to consider when deciding the best fit for your project:

    Clean Architecture

    Clean Architecture emphasizes separation of concerns, making your system easier to maintain and scale. This architecture is designed to keep the business logic independent of the frameworks and tools, which helps in achieving a decoupled and testable codebase.

    See more on Rules to Better Clean Architecture.

    You can find our CA template on GitHub

    Vertical Slice Architecture

    Vertical Slice Architecture structures your system around features rather than technical layers. Each feature is implemented end-to-end, including UI/API, business logic, and data access. This approach improves maintainability and reduces the risk of breaking changes.

    This modular approach to software development can introduce inexperienced teams to the idea of shipping features as functional units with no shared knowledge of the domain entities, infrastructure layer, or application layer within another subsystem, further preparing them for future development environments that may use Modular Monolith or Microservices Architecture.

    You can find our VSA template on GitHub

    Modular Monolith

    A Modular Monolith organizes the system into modules that encapsulate specific functionalities. While it runs as a single application, it retains some benefits of microservices, such as independent module development and testing. It’s a good middle-ground between a monolith and microservices.

    See more on Rules to Better Modular Monoliths.

    Microservices

    Microservices architecture involves splitting the application into small, independently deployable services. Each service focuses on a specific business capability and can be developed, deployed, and scaled independently. This approach is beneficial for complex and large-scale applications with multiple teams working on different parts.

    See more on Rules to Better Microservices.

    Architecture Decision Tree

    architecture decision tree v3
    Architecture Decision Tree

    It's important to keep in mind that these architectures are not mutually exclusive.

    Within a Modular Monolith Architecture, each module could be implemented using Clean Architecture or Vertical Slice Architecture. Similarly, a Microservices Architecture could use Clean Architecture or Vertical Slice Architecture within each service.

    Also, from a pragmatic point of view a combination of Modular Monolith and Microservices might provide the best of both worlds. The majority of the system could be implemented as a Modular Monolith, with a few key services implemented as Microservices to provide scalability and flexibility where needed.

    Factors to Consider

    • Are your requirements certain?
      If requirements are likely to change, Clean Architecture or Vertical Slice Architecture can offer more flexibility.
    • Do you have multiple domains?
      For applications with multiple domains, Modular Monoliths or Microservices can provide better separation and modularity.
    • Do you have many teams? If you have many teams, Microservices or Modular Monolith can help in reducing inter-team dependencies and allow parallel development.
    • Do you need independent deployments? If independent deployments are necessary, Microservices is the best choice due to its isolated nature.
    • Do you need independent scalability? Microservices allow each service to be scaled independently based on its specific needs, which can be more efficient and cost-effective.
    • Do you have DevOps maturity? Microservices require a mature DevOps culture to manage deployments, monitoring, and scaling effectively. Without this, the overhead can be overwhelming.
    • Is the team experienced? The complexity of Microservices can be challenging for less experienced teams. Vertical Slice Architecture although simple, has fewer guardrails when compared to Clean Architecture and can lead to a mess if not managed correctly. This leads to recommending Clean Architecture for less experienced teams that need more structure.

    Examples

    Here are some practical scenarios to illustrate the decision-making process:

    Scenario 1: Startup with uncertain requirements

    You are building an MVP with a small team and expect the requirements to evolve rapidly.

    Choice: Clean Architecture or Vertical Slice Architecture - These architectures offer flexibility and are easier to refactor as requirements change.

    Scenario 2: Medium-sized business with limited DevOps maturity

    You have a mid-sized team, and your organization is still developing its DevOps practices.

    Choice: Modular Monolith - A Modular Monolith provides some modularity benefits without the full complexity of Microservices, making it easier to manage with limited DevOps capabilities.

    Scenario 3: Large enterprise with multiple domains and teams

    You are developing a large-scale application with multiple business domains and have several teams working in parallel.

    Choice: Microservices - Microservices allow independent development, deployment, and scaling, which suits large and complex applications.

    By carefully considering these factors and understanding the strengths and limitations of each architectural style, you can choose the best architecture for your system, ensuring a balance between flexibility, scalability, and maintainability.

  5. Do you regularly review your Microservice boundaries?

    Microservice architecture poses some unique challenges that can often trip up even the most experienced development teams. One of the hairiest problems is creating well-defined boundaries between your services, and the ideal Microservice architecture often differs from what was originally drawn on the whiteboard. So how do you tell if you got it right?

    Map your Microservice architecture

    • Draw your Microservices on a whiteboard
    • Whenever one Microservice needs to communicate with another, draw a line between them. If there are several distinct integration points between the same 2 Microservices, add a "+1" or "+2" or "+N" to the line, indicating how many distinct calls those services share. For every paired Microservice, list all the integration points, with particular focus on why this integration exists, and the frequency in which it occurs (ie rarely, frequently, every time). For example: OrdersService.Create(...)->Customers.GetCustomer(...). Frequency: every request..

    Microservice maps can get intense
    Figure: Incorrect boundaries result in spaghetti services

    Identify hotspots

    • Flag "chatty" services - ie, those services that have the most number of integration points
    • Flag blocking calls - ie, if a particular Microservice must wait on downstream/dependent service call before completing
    • Flag local caches - ie, if Microservice A keeps a local cache of Microservice B's data

    Hotspots are often a code smell that could point to Microservices being tightly coupled. These are telltale signs that the boundaries between those services may be in need of adjusting.

    Compare and contrast with BAU

    In order to measure your Microservice boundary health, you should see what path a "typical" use case takes through your Microservice.A great way of performing this exercise is using your event storming artefacts (if you haven't previously gone through an event storming exercise, it's highly recommended you do so).

    Another great DDD method is context mapping, which helps make context boundaries more explicit.

    Pick the most common or business-critical use case for your business (if there are several, repeat this exercise per use case). Map out your user's journey through your current Microservice ecosystem.

    Reflect and adjust

    You will often find obvious groupings of Microservices divided by "pivotal moments" in your user journey, with a sprinkling of "shared" or cross-boundary services (like an email notification service). These grouped services may better serve your application by being merged into a singular Microservice (or Modular Monolith).

    Don't ignore the problem

    It can be daunting to consider architectural changes to a Microservices application, and difficult to justify such changes to business stakeholders. However, it's an accepted fact in Microservice design that bounded contexts are an incredibly difficult thing to get right, and wrong boundaries can accrue technical debt quickly due to the snowball effect that "hacks" can have on a distributed system. When ignored, this technical debt can rapidly (and covertly) alter your entire system from Microservice architecture to a distributed monolith. At that point, you're in a world of hurt.

    Put the next session in your calendar now

    Microservices require a significantly more architectural discipline than traditional monolithic systems, for the reasons mentioned above. As such, this exercise should be repeated regularly to prevent your microservice ecosystem getting the better of you.

    Even when you do get your boundaries right, they will inevitably change over time as the domain model is changed to reflect new features and insights into the business.

  6. Do you prioritize DevEx in Microservice Architecture?

    You've just started on a microservices-based project. You're excited to dive in, but quickly find yourself lost in a maze of undocumented services. There's no clear way to run or debug the microservices locally, and you're left guessing how to configure your environment. The lack of comprehensive documentation means you spend hours piecing together information from various sources, and every small change requires a tedious setup process. The frustration mounts as you encounter integration issues with other microservices, and there's no one-stop guide to help you troubleshoot. This chaotic environment not only hampers your productivity but also dampens your morale.

    So how can you prevent these problems?

    Microservice architecture can be complex, and ensuring a positive developer experience (DevEx) is crucial for maintaining productivity and morale within your development team. Here are some key areas to focus on to ensure your DevEx is top-notch:

    Ensure your documentation is bullet-proof

    • Comprehensive and up-to-date documentation is essential. Each microservice should have clear and concise documentation that covers:

      • API endpoints and their usage
      • Configuration settings
      • Deployment instructions
      • Common troubleshooting steps
      • Which services are required for which flows (large Microservice environment)

    Keep it accessible

    • Documentation should be easily accessible to all team members. The recommended way is to have a great readme file at the top level of your repo. See our awesome documentation rules for great tips on what to include!
    • If your Microservices span multiple repositories, each repo's readme should have all the information needed to start that particular application in isolation. Instructions on starting multiple applications in unison should be kept in a higher level document - typically a Wiki or other platform that can be linked to from each readme.

    Create a seamless "F5 experience" per microservice

    • Developers should be able to run and debug each microservice locally with minimal setup. This includes:

      • Providing clear instructions for setting up the development environment
      • Using containerization (e.g., Docker) to ensure consistency across different environments
      • Automating common tasks such as database migrations and seeding
      • Minimal reliance on shared or volatile data & services

    Simplify local development

    • Ensure that dependencies are well-managed and that developers can easily spin up any required services or databases locally.

    A great way to tackle this problem is via an Up script ( ie Up.ps1 or Up.sh), where a developer executes the script and has all data, infra, and config automatically provisioned in their development environment.

    If you're building a .NET application, an even better way is using .NET Aspire.

    Foster a culture of collaboration

    • Encourage open communication and collaboration among team members. This can be achieved through:

      • Regular code reviews
      • Pair programming sessions
      • Knowledge-sharing meetings or brown bag sessions

    Use modern tools and practices

    • Adopt modern development tools and practices that enhance DevEx, such as:

      • Continuous Integration/Continuous Deployment (CI/CD) pipelines
      • Automated testing frameworks
      • Code quality tools (e.g., linters, static analysis)

    By prioritizing DevEx, you can create a more efficient, enjoyable, and productive environment for your development team, ultimately leading to better outcomes for your microservice architecture.

We open source.Loving SSW Rules? Star us on GitHub. Star
Stand by... we're migrating this site to TinaCMS