Microservice Architecture

Microservice Architecture
with .NET

Microservice architecture has emerged as a powerful paradigm for building scalable, resilient, and independently deployable applications.

This comprehensive guide explores microservice architecture, its challenges, design considerations, and how the ABP Platform helps developers to create robust microservice solutions.

What is Microservice Architecture?

Microservice architecture is a software design approach where an application is structured as a collection of small, loosely coupled, and independently deployable services. Each service is responsible for a specific business capability, communicates via well-defined APIs, and can be developed, deployed, and scaled independently. Unlike traditional monolithic applications, where all components are tightly interwoven, microservices emphasize autonomy and decentralization.

Microservices align naturally with Domain‑Driven Design (DDD), where each service often represents a bounded context within the domain.

Key Characteristics

Single Responsibility

Each service focuses on a distinct business function (e.g., user management, order processing, payment).

Independent Deployment

A service can be updated or deployed without affecting the entire system.

Decentralized Data Management

Every service typically owns its database or data store, allowing it to choose the best storage technology for its problem while requiring data consistency and integration patterns.

Inter‑service Communication

Services interact using protocols like HTTP, gRPC, or message queues (e.g., RabbitMQ, Kafka).

Advantages

Scalability

Scale individual services based on demand, optimizing resource usage.

Flexibility

Use different technologies (e.g., .NET, Node.js) and databases per service.

Resilience

Isolate failures to specific services, improving overall system reliability.

Team Autonomy

Enable parallel development by independent teams.

Continuous Deployment

Deploy updates to one service without affecting others.

Comparison with Other Architectures

Traditional Monoliths

Microservices architecture builds applications as a set of small, independent services that enhance scalability and flexibility, while traditional monoliths integrate all components into a single, tightly coupled unit that simplifies development but hinders scaling and updates.

Modular Monoliths

Microservices architecture designs applications as a collection of independently deployable services, prioritizing scalability and autonomy, while modular monoliths organize functionality into distinct internal modules within a single deployment, balancing structure with simplicity.

Challenges of Microservice Development

While microservices offer significant benefits, they introduce complexities that require careful planning and robust infrastructure. Below are the primary challenges developers face.

Determining Boundaries of Services

Determining service boundaries is one of the most challenging parts of the microservice approach. Understanding which functionality should be implemented in which service can take time. Once you better understand the system, you may need to move some entities or APIs fully or partially to another service. Sometimes, that can also happen when business requirements change. Making such changes can be difficult, especially if the solution has already been deployed and used in production.

So, microservice systems are suggested for mature domains. Consider starting with a monolith modular application if your service boundaries are unclear or your business is not mature.

Development Time Complexity

Setting up a distributed system's development environment can be challenging. Developing, debugging, testing, running, and troubleshooting problems for a single service that interacts with other services and components is not as easy as working on a monolithic application. You need to spend extra effort to set up an efficient development environment for microservice development.

Team Coordination

Microservices often involve multiple teams working on different services, necessitating clear API contracts, versioning strategies, and alignment on domain boundaries.

Data Consistency

With each service managing its own database, ensuring data consistency across services becomes challenging. Traditional ACID transactions are replaced by eventual consistency models, often requiring saga or inbox/outbox patterns, or compensating transactions.

Optimizing Performance

While the first promise of microservice architecture is scalability, a single operation’s performance is generally worse than a monolithic application, especially if the operation requires touching multiple services and databases. This is mostly because of network delays, aggregation of distributed data, service discovery, man-in-the-middle services, etc. If you don’t consider performance seriously, you may end up with a system with too much delay on user requests.

You need to care about performance on every inter-service interaction and optimize these communications. Consider using caching instead of querying every time. You can even implement data duplication (copying other services’ data into a service’s database to reduce communication and optimize query performance), denormalization, and synchronization (on data changes on the target service).

Service Discovery

As the number of services grows, finding the correct endpoint for each service can be challenging. Solutions include service discovery tools or an API Gateway approach.

Runtime Complexity

Microservices operate as a distributed system, requiring coordination across multiple services. This introduces latency, network failures, and the need for reliable inter-service communication.

Deployment and Orchestration

Managing multiple services, each with its own lifecycle, dependencies, and scaling needs, demands sophisticated deployment pipelines and orchestration tools like Kubernetes.

Monitoring and Debugging

Tracking issues across distributed services requires centralized logging, distributed tracing, and comprehensive monitoring to pinpoint failures or bottlenecks. Tools like OpenTelemetry, Jaeger, or Prometheus are often needed.

Who is Microservice Architecture Suitable For?

Microservices are not a one-size-fits-all solution. They shine in specific scenarios but may overcomplicate simpler projects.

Consider microservices:

If your business domain is mature.
If your domain is too complex to develop and maintain in a single monolith codebase.
If your business domain is possible to split into sub-domains.
If you have multiple teams that will work on the solution in parallel.
If you need to develop, test, deploy, and scale services independently.
If you need to serve too many users concurrently with a high-availability and fault-tolerant system.
If you need to use multiple technology stacks so, some services can be built with .NET, and others can be built with Java, Python, etc...
If you have DevOps knowledge and culture in your company, and if you can deal with complex development, build, test, deployment and production environments.

Starting with a Monolith-first for Microservice Architecture

If you are considering building a microservice architecture, it is usually advised to start with a monolith modular first, then migrate to microservices later once your business and module boundaries are more stable.

See modular monolith architecture documentation to learn more about that architecture and what ABP provides as infrastructure to build modular systems.

Designing a Microservice Solution

Once you decide to start with a microservice system, you need to make some fundamental decisions. These decisions shape the organization of your database, codebase, and development teams. In the next sections, we will explore these design points to understand our options and their pros and cons.

The Overall System Components

It is good to understand the overall structure of a microservice system. The diagram below shows the main components of an example microservice solution:

Overall System Components

In a typical microservice solution, you have applications (web, mobile, etc.), API gateways (typically one for each application or client type), microservices, databases, infrastructure services (e.g., RabbitMQ, Redis, Elasticsearch) and more.

The Database Architecture

A microservice system's database design is significantly different from a classic monolith application.

Database per Service

In a microservice system, it is a best practice that every service manages its own data and does not directly access other services’ data. Otherwise, it would break the main purpose of modularity and make services coupled to each other's internal implementation details, leading to a complicated codebase. When a service depends on the internal details of a service, any change in the target service may easily break the dependent service.

Migrating Database Schemas

If you use a relational database (or a type of database that requires a pre-defined database schema), any change to the database schema (e.g., adding a new field to a database table or introducing a new table) requires updating the database while deploying the new version of the service. It is best to automate the migration process when you deploy your services. Managing its own database schema for a service is typically a good practice in a microservice system to ensure autonomy and consistency.

Dealing with Distributed Transactions and Data Consistency

If you have a single database and your database provider supports ACID transactions, you can simply perform transactions for multi-query operations to ensure data consistency. However, when you have a distributed system with multiple databases, you should carefully design operations that make changes in multiple databases. Using distributed events for side effects (when a change in a service requires another change in another service, it is called a side effect) and implementing the outbox and inbox patterns can be a solution.

Structure of a Service Codebase

A typical microservice system will contain many services. Each service may use different technologies (.NET, Node.js, etc.) and internal structures. Depending on its complexity, a service can consist of a single function, a single-layer codebase, a layered codebase, etc.

A typical . NET-based microservice project can consist of a solution structure that is shown below:

Solution Structure

Let’s investigate the projects of that .NET solution.

Implementation (CloudCrm.OrderingService)

This project contains the actual implementation of the service. It includes entities, data access codes, application services, and other internal details.

Interface (CloudCrm.OrderingService.Contracts)

This project contains service interfaces, data transfer objects, event definition classes, and other objects you want to share with other services. You can deploy this project as a NuGet package. Other services can then use this project to communicate with that microservice.

Sharing such a Contracts package simplifies your development flow and allows you to share types between different services. If you think that is an unnecessary dependency, you can discard that package and use the service’s API without sharing any objects. In that case, the client service can define similar DTOs for its own usage (or it can use some code generation tools to generate proxies for HTTP or gRPC services).

Tests (CloudCrm.OrderingService.Tests)

If you are considering building automated tests, this project contains automated (unit/integration) tests related to the service.

Communication Between Services

In a microservice system, these services communicate with each other. Since each microservice is a standalone executable process, it needs to communicate with inter-process communication techniques. In the next sections, we will discuss these communication techniques.

Synchronous Communication

Use REST or gRPC for direct, real-time interactions. Ideal for simple request-response scenarios but can introduce coupling. Consider the following points while implementing such communication between two services:

Read only: Synchronous communication is mainly for reading/querying data from other services. They are not for making changes in other services. If you need to make a change in the other service, consider using asynchronous messaging.

Caching: Use caching if you frequently request the same data from a service. If multiple services use the same service for querying purposes, it can be a bottleneck in the system and decrease the overall performance.

Design integration APIs: Do not reuse the APIs that are designed to be used by the presentation layer. Otherwise, changes done to the UI may break your integrations, or changes in the integration logic may break your UI layer. It is best to follow the Single Responsibility Principle and create separate integration services for integration purposes, even if the initial logic is similar.

Asynchronous Communication

A service can subscribe to another service's events. Once an event is published, it can take the necessary action. We can use an event bus service (e.g., RabbitMQ, Kafka) to orchestrate that communication.

Events are best if a change in a service triggers another change in other services. By the nature of the “one publisher multiple subscriber” logic, multiple services can listen to the same event and take different actions.

To ensure data consistency and publishing events in a transactional manner, you can use the inbox/outbox patterns.

The User Interface

There are several options for the user interface, each with its own pros and cons.

Monolith UI

A microservice system can have a monolithic user interface application. That UI application can use the API Gateway to interact with the system's services.

A monolithic UI can be a good choice if your application consists of pages that interact with many services. Its less complex structure makes it easier to understand and develop, especially when a single team of developers is working on it.

Modular UI

You can build multiple UI modules and bring them together to be used as a single web application. These modules may match with the microservices or can be independent of the underlying service distribution.

A modular UI can be a good choice if every module uses one or a few services, doesn’t need to interact with other modules’ UI, and you have different teams working on different areas of your application.

Micro Frontends

As an additional step on the modular UI, every module can be separately hosted as an application. These applications typically serve UI components or pages. An umbrella UI application can consume these applications and provide a single application experience to the end user.

This approach offers improved scalability, faster development cycles, and easier maintenance, as teams can work autonomously and deploy their micro-frontends independently without affecting the entire application. However, it introduces challenges like ensuring consistent design across independently developed components, managing inter-team communication, and handling runtime integration complexities. Micro-frontends are particularly well-suited for large-scale applications or organizations with distributed teams, providing flexibility and resilience at the cost of added coordination overhead.

Hybrid Approach

Instead of getting stuck in one of the options, you can take advantage of different approaches in different areas of your UI application. Relatively independent or generic functionalities (e.g. user management, audit logging, or chat) can be built as separate modules, you can build some reusable components, or even host some components or pages in different frontend applications and render them into the main application to provide a unified user experience.

Enable Observability

A distributed system such as a microservices solution needs to be well thought out in terms of logging, monitoring and tracing.

You can centralize logging with tools like Elasticsearch or Seq. You can implement distributed tracing (e.g., Jaeger, Zipkin) to track requests across services, and you can monitor health and performance with metrics Prometheus and Grafana.

Automate Deployment

Deploying is another challenge of distributed systems. You need to have a good DevOps culture and need to setup some CI/CD pipelines. You typically need to containerize your services with Docker and orchestrate them using Kubernetes for scalability and resilience.

Monorepo vs Multirepo

One of the most important design decisions in microservice development is whether to store your codebase in a single Git repository or distribute it across multiple Git repositories.

When you look at that question first, you may think it is natural that each microservice is developed in a separate Git repository to ensure complete code isolation and parallel development. However, when you start implementing it, you can easily understand how it complicates development, testing, versioning, and deployment.

In the next sections, we will discuss the pros and cons of both approaches and suggest when to use them.

The Monorepo Approach

If you implement the following structure, then you use the monorepo approach;

  • The source code of all the services is stored in a single repository.
  • Services depend on each other with local project references (contract/interface dependencies).
  • All the services are in the same version and deployed together.

Here are the pros of the monorepo approach;

  • Access to changes immediately: When you make a change (bugfix or new feature), you can immediately use it in other services. This dramatically reduces the integration efforts of multiple services. You don’t need to wait for the other module to be versioned and deployed to use its new features.
  • Implementing backward compatibility is unnecessary since all the services use the latest versions of other services.

So, it is much easier to develop, version, test and deploy a modular application in a monorepo approach. Especially if you have a small team, each developer is working on multiple services, and these services and their communication ways frequently change (which is normal at the beginning of a project), it is best to start with the monorepo approach.

However, when you use the monorepo approach, you lose one of the main benefits of microservices: Independent deployment. So, you can start with monorepo, then evolve to multiple repositories as your system matures.

The Multirepo Approach

In a multirepo approach,

  • Every service is located in a separate Git repository.
  • Services use each other via package (e.g., NuGet, NPM) references (if you share contract/interface packages with other services), and each service uses the other’s specific version.
  • Services are versioned and deployed separately.

Notice that, sometimes, you can group a few services into a repository. In that case, the repository containing these services should be considered a partial monorepo. All services in that repository should use each other as local project references and be versioned and deployed together. You can have many groups of services like that, where each group is located in a separate repository, and you finally have many repositories.

In the following conditions, you may consider using the manyrepo approach:

  • You want to develop, test and deploy your services independently.
  • You have many teams (or departments) specialized in different services.
  • Services are mature, boundaries are stable, and integration is limited.
These suggestions assume that you started with a microservice solution from scratch. However, most of the time, it is suggested that you start with a modular monolithic application first, then migrate your solution to microservices when it is mature enough.

Infrastructure Services or Libraries

In a microservice system, it is essential to understand the difference between business services and infrastructure services:

Business Services: These services implement the business values and application functionalities that you provide to your users. Examples: Ordering, catalog management, shipping, payment, product reviews, search, etc.

Infrastructure Services: These services are generic and technical, not directly related to your business but necessary to support your business application. Examples: Authorization, localization, caching, BLOB storing, audit logging, etc.

It is typical that you have separate microservices for each business functionality. However, when it comes to infrastructure services, they can be either implemented as standalone services or a set of shared libraries.

Implementing Infrastructure as a Service

For example, checking if the current user has a specific permission (a.k.a. authorization) is a typical example operation that is almost needed by all of your microservices. You can implement an authorization service and call it whenever you want to check a permission inside a microservice operation.

Here is an example diagram that shows this architecture:

Example Diagram 1

Whenever the Catalog microservice gets a request to edit a product detail;

  • The Catalog microservice performs a gRPC (or HTTP) request to the Authorization service to check if the current user (who performs the current request) has granted permission to edit products.
  • The Authorization service performs some permission-checking logic. When it needs to get permission data from the database, it can first check a cache (e.g., Redis cache).
  • If the needed permission data was already stored in the cache, no database call is performed. Otherwise, it performs a database query (and caches it for next operations), completes the operation and returns result to the Catalog microservice.

This architecture looks good at first look, and can be applicable for some scenarios, but we think it has some problems:

  • Almost all operations of all microservices require some level of permission-checking, which makes the Authorization service a bottleneck in the system since it will be executed in every operation and will get too much load.
  • It slows down the request in the Catalog microservice since a remote network call is included in every request.
  • Any failure in the authorization service stops all other services, which makes the authorization service a single point of failure.
  • It will be hard to implement custom authorization logic that depends on the data of the microservice that makes the authorization check.

The authorization service is not the only common infrastructure service. Localization, settings, audit logging, BLOB storage, and many other infrastructure services can exist in a microservice system. So, think that every call to every microservice will make additional network requests to 5-6 services.

An alternative approach is to implement these infrastructure requirements as shared libraries.

Implementing Infrastructure as a Library

Instead of deploying a separate service, you can implement your infrastructure requirements as a set of reusable libraries (or a framework) and use these libraries in your microservices as package references.

Let’s take the same authorization example. The following diagram shows how the Catalog microservice can check a permission:

Example Diagram 2

Whenever the Catalog microservice gets a request to edit a product detail;

  • The Catalog microservice performs an in-process method call to check if the current user (who performs the current request) has granted permission to edit products.
  • The Authorization service performs some permission-checking logic. When it needs to get permission data from the database, it can first check a cache (e.g., Redis cache).
  • If the needed permission data was already stored in the cache, no database call is performed. Otherwise, it performs a database query (and caches it for next operations), completes the operation and returns result to the Catalog microservice.

With such an architecture:

  • The authorization checking process will be significantly faster since it doesn’t include a remote call over the network.
  • Implementing custom authorization logic is simpler since the default authorization logic works in-process and can easily use your data ot additional business logic.
  • Authorization logic is still decoupled from your business logic, so you can develop and manage it separately.

Like almost every decision in software development, this architecture decision also has drawbacks in some conditions:

  • It may not be possible to share a single authorization library among your microservices if they have been developed in different technologies (.NET, Node.js, Python, etc)
  • The authorization library should be developed as backward compatible since multiple versions of that library may work in different microservices.

In addition, you may ask if it is a good idea to store the permission data inside the Catalog database to provide full autonomy, so it doesn’t have a dependency on a second database. We think that would make managing user permissions in a central application and maintaining the database structure harder. Also, it gives too much responsibility to the Catalog database. Think that authorization is not the only infrastructure requirement.

So, we think implementing such infrastructure requirements as shared libraries is a good balance for most of the scenarios. However, if your case requires a different architecture, carefully consider the pros and cons of all your options.

Need for an Infrastructure

When you have a monolith application, your infrastructural code becomes a part of the application. In a good implementation, it can be in a separate layer. But even in that way, it is implemented specifically for that application’s requirements.

However, it is not reasonable to repeatedly re-implement the infrastructure requirements in every service of a microservice solution. You need to have an infrastructure framework to implement common requirements. The following sections explain some of these common requirements.

Base Classes & Utilities

At a very basic level, you want some base classes to inherit your entities, repositories, application services, API controllers, data transfer objects, and utility classes to perform simple common tasks. Collecting these classes in a shared set of libraries makes your services’ code simpler and less repetitive and forces them to be implemented more consistently.

Automating Cross-Cutting Concerns

Some common operations should be implemented in every use case: authorization, validation, database connection and transaction management, exception handling, audit logging, concurrency check, response caching, and sometimes more…

The implementation of these common concerns is very similar to that of every other use. In a well-designed application, these concerns are implemented in a single place and then applied to every use case through interceptors, action filters, middlewares, or a similar mechanism. So, these implementations should also be located in the shared libraries (or framework).

Abstracting 3rd Party Libraries

Every business application needs many third-party libraries and services to perform common tasks, such as dependency injection, object mapping, distributed caching, SMS and email sending, scheduling background jobs, data access, PDF/Excel export, BLOB storage, image manipulation, etc.

A well-designed application has abstractions for these kinds of libraries and services for the following reasons;

  • Simplifying and standardizing the configuration and usage of the library and service will make it easy to change the configuration at a single point. Also, the application code will be much simpler to implement and will not care about the details of the library API.
  • When you need to change or upgrade a library and service, it will be much easier since the library usage code is not distributed throughout your application.
  • Mocking the library access code in your unit tests will be possible.

So, abstraction and integration code for such libraries and services should also be located in some shared libraries.

Enterprise Building Blocks

Every serious application needs common building blocks, like setting management, permissions management, audit logging, event bus, data filtering, maybe multi-tenancy infrastructure, and more. Since all these features can not be part of a particular service, you need to implement them in your infrastructure framework.

The Infrastructure (Framework) Team

Because of all these (and more) common requirements, it is typical for a company to have an infrastructure (or framework) team working on the infrastructure layer. Otherwise, it is difficult to determine a standard between the services, separate infrastructural requirements from the services, and implement them in a shared infrastructure layer.

Instead of having such an infrastructure team and building all these common infrastructures yourself, consider using an application framework designed specifically to solve these problems and provide the necessary infrastructure for your application.

In the next section, we will briefly explain how the ABP Platform helps you to overcome these challenges.

Microservices with the ABP Platform

The ABP Platform has been designed to provide a complete infrastructure for building microservice applications. The next sections highlight some ABP Platform components and explain how they help you build a microservice solution successfully.

ABP: A Modular and Microservice Compatible Framework

The core ABP Framework is open-source and provides the fundamental infrastructure for modular and microservice development.

The Infrastructure for All Your Needs

ABP provides a comprehensive infrastructure for common business application requirements, not only for microservice applications but also for any kind of serious software solutions. Instead of having an internal infrastructure team, let the ABP development team be your infrastructure team.

Audit logging, background jobs, BLOB storing, email sending, advanced setting and permission management, base classes for business objects, many 3rd party library and service integrations, automating all the cross-cutting concerns, and more… Check the ABP documentation to see what you can directly use and then focus on implementing only your business needs.

Communication Infrastructure

ABP provides the fundamental models to facilitate communication between microservices.

The event bus is used for loosely coupled communication between the microservices. It provides distributed messaging with distributed transaction support. Many common distributed message brokers are already integrated (e.g., RabbitMQ, Kafka, Rebus, Azure Service Bus).

ABP’s auto API controller system makes it straightforward to serve your REST APIs with versioning support. The Dynamic C# API Client proxy system simplifies consuming REST APIs from client applications or other microservices. The Openiddict integration allows you to authorize client to client communication for sensitive operations.

Modularity on User Interface

It is a common approach to build a microservice solution’s UI as modular.

ABP supports multiple UI technologies (e.g. MVC / Razor Pages, Blazor and Angular), and for each technology it provides a complete modular architecture to build UI pages/components in different modules and compose them in a unified web application.

ABP separates UI theming/styling from module code. It determines and standardizes how to build UI pages and components in module packages. This way, all modules use the same design language and can work with any UI theme that applies the same standards.

Menus, toolbars, layout hooks, alerts, localization, client-side package dependencies, the bundling/minification system, and many other UI parts are designed to support multi-module scenarios and allow you to shape your application UI dynamically.

Domain-Driven Design Infrastructure

Domain-Driven Design (DDD) is a widely accepted approach to building complex and modularr/microservice software solutions. DDD is a first-class citizen in the ABP Framework. ABP provides base classes and services to build your aggregates/entities, repositories, domain services, application services, and more.

You can design your microservices and communicate them with each other using bounded context and context mapping principles. If you prefer to implement DDD principles and patterns, ABP can help you practically implement your technical requirements. You can also download the free Implementing Domain-Driven Design e-book that clearly explains how to shape DDD building blocks in your application code.

Pre-Built Application Modules

In addition to simplifying and accelerating your modular application development, ABP also provides pre-built application modules ready to install and use in your application. The good part is that all these modules are designed to be compatible with microservice scenarios. Every module can work as a standalone service.

You don’t have to reinvent the wheel in every application. For example, the account module provides user login, user register, forgot password, email confirmation, two-factor authentication, user lockout, social logins, active directory integration, OpenID connect integration, user impersonation and more… Just install it to your application, and you have an advanced account system. You can check the other modules to understand how they can save you time.

ABP Studio: Architect and Build Modular Applications

ABP Studio is a cross-platform desktop application for ABP and .NET developers. It aims to provide a comfortable development environment by automating tasks, providing insights about your solution, and making it much easier to develop, run, browse, monitor, trace and deploy your solutions.

One of the main goals of ABP Studio is to architect and build modular .NET solutions. You can easily create a new microservice solution and add services to your solution.

The Microservice Solution Template

Last but not least, ABP provides a pre-architected, well-designed, production-ready microservice solution template for creating new microservice solutions. It also provides documentation, guides and tutorials to clearly explain how to understand and develop your solution with that startup template and add new microservices and applications.

ABP Studio

Conclusion

Microservice architecture offers unparalleled flexibility, scalability, and resilience for modern applications, but comes with significant challenges. We’ve built the ABP Platform to address these complexities head-on, providing a robust, battle-tested infrastructure for .NET developers. Whether you’re starting a new project or migrating an existing one, ABP Platform empowers you to harness the full potential of microservices with confidence.


Get Started with Your Microservice Application Today

Use the following resources to start building your microservice application today using the ABP Platform: