Skip to main content

Monorepo vs Multi-repo for Microservices

Overview

In a microservices architecture, your repository strategy significantly impacts development efficiency, CI/CD, and team collaboration. The two main approaches are Monorepo and Multi-repo (Polyrepo), each with different trade-offs.


Monorepo

An approach where all microservices and libraries are managed in a single repository.

my-platform/
├── services/
│ ├── OrderService/
│ │ ├── src/
│ │ ├── tests/
│ │ └── OrderService.csproj
│ ├── UserService/
│ │ ├── src/
│ │ ├── tests/
│ │ └── UserService.csproj
│ └── PaymentService/
│ ├── src/
│ ├── tests/
│ └── PaymentService.csproj
├── shared/
│ ├── Common.Contracts/
│ ├── Common.Infrastructure/
│ └── Common.Testing/
├── Directory.Build.props
├── Directory.Packages.props
└── MyPlatform.slnx

Advantages

AspectDescription
Easy code sharingCommon libraries and contracts can be used via direct project references
Atomic changesCross-service changes can be made in a single PR
Unified version managementPackage versions centrally managed with Directory.Packages.props
Easier refactoringAPI contract changes can be applied across all services at once
Consistent developer experienceCoding standards, linting, and CI configurations are unified

Disadvantages

AspectDescription
Repository bloatCodebase grows large, slowing clones and builds
CI complexityRequires mechanisms to build/deploy only affected services
Access control difficultyFine-grained per-service access control is challenging
Coupling riskIndependent deployment becomes harder; tight coupling may creep in
Tooling dependencyDedicated build tools (Nx, Bazel, etc.) may be needed

Multi-repo

An approach where each microservice is managed in its own independent repository.

# Individual repositories
order-service/
├── src/
├── tests/
├── Dockerfile
├── OrderService.csproj
└── OrderService.sln

user-service/
├── src/
├── tests/
├── Dockerfile
├── UserService.csproj
└── UserService.sln

common-contracts/ # Published as NuGet package
├── src/
├── Common.Contracts.csproj
└── Common.Contracts.sln

Advantages

AspectDescription
Independent deploymentEach service can be built and deployed fully independently
Clear ownershipEach team owns their repository with clear responsibility boundaries
Simple CI/CDEach repository's CI pipeline is small and simple
Flexible access controlPermissions can be configured per repository
Technology freedomDifferent frameworks or versions can be chosen per service

Disadvantages

AspectDescription
Cumbersome code sharingCommon libraries must be packaged as NuGet packages
Cross-cutting change costRequires PRs across multiple repositories with high coordination cost
Version inconsistencyDependency package versions may diverge across services
Code duplicationCommon logic tends to be duplicated across repositories
Onboarding difficultyHard for newcomers to understand which repository contains what

Comparison Matrix

AspectMonorepoMulti-repo
Code sharing✅ Easy via project references⚠️ Requires NuGet packaging
Atomic changes✅ Single PR❌ Multi-repo coordination needed
Independent deployment⚠️ Complex CI configuration✅ Naturally independent
Team autonomy⚠️ Potential conflicts✅ Fully independent
CI/CD complexity⚠️ Impact detection required✅ Simple
Onboarding✅ Everything in one place⚠️ Distributed
Tooling requirements⚠️ May need specialized tools✅ Standard tools suffice
Refactoring✅ Bulk changes are easy❌ High coordination cost
Repository size❌ Grows large✅ Stays small
Access control❌ Hard to restrict✅ Per-repository control

.NET Monorepo Best Practices

1. Shared Settings with Directory.Build.props

Place a Directory.Build.props at the repository root to apply common settings across all projects.

<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>

2. Central Package Management (CPM)

Centrally manage NuGet package versions with Directory.Packages.props.

<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="FluentValidation" Version="11.11.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
</Project>

Individual projects can omit version numbers.

<!-- OrderService.csproj -->
<ItemGroup>
<PackageReference Include="Serilog" />
<PackageReference Include="FluentValidation" />
</ItemGroup>

3. Change Impact Detection (GitHub Actions)

Build CI/CD pipelines that only build and deploy affected services.

# .github/workflows/ci.yml
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
order-service: ${{ steps.changes.outputs.order-service }}
user-service: ${{ steps.changes.outputs.user-service }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
order-service:
- 'services/OrderService/**'
- 'shared/**'
user-service:
- 'services/UserService/**'
- 'shared/**'

build-order-service:
needs: detect-changes
if: needs.detect-changes.outputs.order-service == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- run: dotnet build services/OrderService/OrderService.csproj
- run: dotnet test services/OrderService/tests/

4. Leveraging Solution Files

Use the .slnx format (available from .NET 9) and leverage per-service filtering.

<!-- MyPlatform.slnx (full solution) -->
<Solution>
<Folder Name="/services/OrderService/">
<Project Path="services/OrderService/OrderService.csproj" />
<Project Path="services/OrderService/tests/OrderService.Tests.csproj" />
</Folder>
<Folder Name="/services/UserService/">
<Project Path="services/UserService/UserService.csproj" />
<Project Path="services/UserService/tests/UserService.Tests.csproj" />
</Folder>
<Folder Name="/shared/">
<Project Path="shared/Common.Contracts/Common.Contracts.csproj" />
<Project Path="shared/Common.Infrastructure/Common.Infrastructure.csproj" />
</Folder>
</Solution>

You can also provide per-service solution files for individual development.

<!-- services/OrderService/OrderService.slnx (individual development) -->
<Solution>
<Project Path="OrderService.csproj" />
<Project Path="tests/OrderService.Tests.csproj" />
<Project Path="../../shared/Common.Contracts/Common.Contracts.csproj" />
</Solution>

5. Shared Library Design Guidelines

Shared libraries in a monorepo should follow these principles.

shared/
├── Common.Contracts/ # DTOs, event definitions (no dependencies)
├── Common.Infrastructure/ # Cross-cutting concerns (logging, auth, health checks)
└── Common.Testing/ # Test helpers, custom factories
  • Contracts should consist only of POCOs with no external dependencies
    • POCO (Plain Old CLR Object) refers to a pure C# class (or record) that does not depend on any specific framework or base class. Because POCOs have no references to external libraries or framework-specific attributes, they can be referenced from any project with minimal overhead, avoiding serialization and versioning issues
  • Infrastructure should remain a thin abstraction layer, overridable by each service
  • Do not place service-specific logic in shared libraries
// ✅ Suitable for sharing: event contracts
namespace Common.Contracts.Events;

public record OrderCreatedEvent(
Guid OrderId,
Guid CustomerId,
decimal TotalAmount,
DateTime CreatedAt);

// ❌ Not suitable for sharing: service-specific business logic
// namespace Common.Services;
// public class OrderPricingCalculator { ... }

.NET Multi-repo Best Practices

1. Code Sharing via NuGet Packages

Publish common libraries as NuGet packages for consumption by each service.

<!-- Common.Contracts.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<PackageId>MyCompany.Common.Contracts</PackageId>
<Version>2.1.0</Version>
<Description>Shared contracts and DTOs for microservices</Description>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
</Project>

Use GitHub Packages or Azure Artifacts as private NuGet feeds.

<!-- nuget.config -->
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="MyCompanyFeed"
value="https://pkgs.dev.azure.com/myorg/_packaging/myFeed/nuget/v3/index.json" />
</packageSources>
</configuration>

2. Template Repositories

Prepare template repositories to standardize new microservice creation.

service-template/
├── src/
│ ├── Program.cs
│ ├── appsettings.json
│ └── MyService.csproj
├── tests/
│ └── MyService.Tests.csproj
├── .github/
│ └── workflows/
│ └── ci.yml
├── Dockerfile
├── .editorconfig
├── nuget.config
└── README.md

Packaging as a dotnet new template is also effective.

dotnet new install MyCompany.ServiceTemplate
dotnet new myservice -n InventoryService

3. Contract Testing

In a multi-repo setup, Contract Testing is crucial for ensuring API contracts between services don't break.

// Example of consumer-driven testing with Pact
public class OrderServiceConsumerTests
{
private readonly IPactBuilderV4 _pactBuilder;

public OrderServiceConsumerTests()
{
var pact = Pact.V4("OrderService", "UserService", new PactConfig());
_pactBuilder = pact.WithHttpInteractions();
}

[Fact]
public async Task GetUser_ReturnsExpectedUser()
{
_pactBuilder
.UponReceiving("a request for user details")
.WithRequest(HttpMethod.Get, "/api/users/123")
.WillRespond()
.WithStatus(HttpStatusCode.OK)
.WithJsonBody(new
{
id = "123",
name = "John Doe",
email = "john@example.com"
});

await _pactBuilder.VerifyAsync(async ctx =>
{
var client = new HttpClient
{
BaseAddress = ctx.MockServerUri
};
var response = await client.GetAsync("/api/users/123");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
});
}
}

4. Automated Dependency Updates with Renovate / Dependabot

In a multi-repo setup, dependency management is distributed, so introduce automated update tools.

// renovate.json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"nuget": {
"enabled": true
},
"packageRules": [
{
"matchPackagePatterns": ["MyCompany\\.Common\\..*"],
"automerge": true,
"automergeType": "pr"
}
]
}

Hybrid Approach

In real-world projects, a hybrid approach combining monorepo and multi-repo can be effective.

Hybrid Pattern Examples

PatternDescription
Platform + Domain separationCommon infrastructure in monorepo, domain services in multi-repo
Team-based monoreposEach team manages their owned services in a monorepo
Shared libraries only monorepoShared code in monorepo, individual services in multi-repo

Selection Guidelines

When Monorepo is a Good Fit

  • Small teams (~20 people) working closely together
  • Strong inter-service dependencies with frequent simultaneous changes
  • High frequency of shared library changes
  • Organization values unified development standards and toolchains
  • Using .NET Aspire for unified orchestration

When Multi-repo is a Good Fit

  • Large organizations with multiple autonomous teams
  • Services developed and deployed independently by each team
  • Different technology stacks or release cycles needed
  • Strict access control or compliance requirements
  • External vendors or partners developing services

Decision Flowchart


Summary

  • Monorepo excels at code sharing and atomic changes but faces CI/CD complexity and repository bloat challenges
  • Multi-repo excels at team autonomy and independent deployment but faces cross-cutting change and version management challenges
  • .NET features like Directory.Build.props, Central Package Management, and .slnx powerfully support monorepo operations
  • For multi-repo, NuGet packaging, template repositories, and Contract Testing are key
  • There is no single right answer: Choose the appropriate strategy based on team size, organizational structure, and service dependencies

References