# Shared Kernel Pattern in Modular Monoliths

> Modules need to share some code without becoming coupled. The shared kernel pattern defines a small, explicit set of shared types that all modules can depend on.

Published: 2026-08-13. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith

The shared kernel pattern gives modules a middle path between sharing nothing and sharing everything: a small, explicit, governed set of types that every module can safely depend on.
In a .NET modular monolith, it's typically a single project with base domain types, common interfaces, and the result pattern.
This article covers what goes in, what stays out, and how to keep it from growing into a "Common" project.

## The Sharing Dilemma

In a [**modular monolith**](https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet), modules should be independent. But they inevitably need to share some things - base entity classes, common interfaces, integration event contracts, and [**value objects**](https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals).

The question isn't whether to share, but how much and how.

Teams tend toward one of two extremes: modules that share nothing (duplicating hundreds of lines of identical code) and modules that share everything (a giant "Common" project that defeats the purpose of modularity). The shared kernel pattern sits in the middle.

## What Is a Shared Kernel?

The shared kernel is a [**DDD concept**](https://milanjovanovic.tech/blog/bounded-context-ddd-explained) - a small, well-defined set of code that two or more bounded contexts agree to share. It's not a dumping ground for convenience. Every type in the shared kernel is there because multiple modules genuinely need it.

In a .NET modular monolith, the shared kernel is typically a single project that all modules reference:

![Dependency diagram showing the Catalog, Ordering, and Shipping modules all referencing a single Shared Kernel project that holds base types, the Result and Error types, IDomainEvent, and strongly typed IDs](https://milanjovanovic.tech/blogs/articles/shared-kernel-pattern-modular-monolith/shared-kernel-dependencies.png)

```
src/
  SharedKernel/
    SharedKernel.csproj
  Modules/
    Catalog/
    Ordering/
    Shipping/
```

## What Belongs in the Shared Kernel

### Base Domain Types

Abstract base classes for **entities**, **aggregate roots**, and value objects:

```csharp
public abstract class Entity
{
    public Guid Id { get; protected init; }

    private readonly List<IDomainEvent> _domainEvents = [];

    public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    public void AddDomainEvent(IDomainEvent domainEvent)
    {
        _domainEvents.Add(domainEvent);
    }

    public void ClearDomainEvents()
    {
        _domainEvents.Clear();
    }
}

public abstract class AggregateRoot : Entity
{
    // Aggregate-specific behavior
}
```

### Common Interfaces

Interfaces that define cross-cutting contracts:

```csharp
public interface IDomainEvent
{
    Guid EventId { get; }
    DateTime OccurredOnUtc { get; }
}

public interface IIntegrationEvent
{
    Guid EventId { get; }
    DateTime OccurredOnUtc { get; }
}

public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
```

### The Result Type

A [**result pattern**](https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern) implementation that all modules use for error handling:

```csharp
public class Result
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public Error Error { get; }

    protected Result(bool isSuccess, Error error)
    {
        IsSuccess = isSuccess;
        Error = error;
    }

    public static Result Success() => new(true, Error.None);
    public static Result Failure(Error error) => new(false, error);
    public static Result<T> Success<T>(T value) => new(value, true, Error.None);
    public static Result<T> Failure<T>(Error error) => new(default!, false, error);
}

public class Result<T> : Result
{
    public T Value { get; }

    protected internal Result(T value, bool isSuccess, Error error)
        : base(isSuccess, error)
    {
        Value = value;
    }
}

public record Error(string Code, string Description)
{
    public static readonly Error None = new(string.Empty, string.Empty);
}
```

### Integration Event Contracts

The event types that define the contract between modules for [**event-driven communication**](https://milanjovanovic.tech/blog/event-driven-communication-modules):

```csharp
// These are the contracts between modules
public sealed record OrderPlacedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount) : IIntegrationEvent;

public sealed record PaymentCompletedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    decimal Amount) : IIntegrationEvent;
```

An alternative worth considering: keep only the `IIntegrationEvent` interface in the shared kernel and put each module's event records in that module's `Contracts` project (so `OrderPlacedIntegrationEvent` lives in `Ordering.Contracts`).
That scopes each contract to its owning module, at the cost of consumers referencing multiple Contracts projects.
Both work; just pick one convention and stick to it.

### Strongly Typed IDs

If you're using **strongly typed IDs** that cross module boundaries:

```csharp
public readonly record struct CustomerId(Guid Value)
{
    public static CustomerId New() => new(Guid.NewGuid());
}

public readonly record struct ProductId(Guid Value)
{
    public static ProductId New() => new(Guid.NewGuid());
}
```

## What Does NOT Belong in the Shared Kernel

This is where teams go wrong. The shared kernel should not contain:

- **Module-specific domain logic** - An `Order` entity belongs in the Ordering module, not the shared kernel
- **Infrastructure concerns** - Database configurations, HTTP clients, logging helpers
- **Utility classes** - String helpers, date formatters, extension methods that only one module uses
- **DTOs/ViewModels** - These are API concerns, not domain concepts

A good rule of thumb: if removing a type from the shared kernel breaks only one module, it doesn't belong there.

## Project Structure

Keep the shared kernel minimal and well-organized:

```xml
<!-- SharedKernel.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>

  <!-- Zero external dependencies! -->
</Project>
```

The shared kernel should have zero external NuGet dependencies. It's pure domain code. If you need EF Core or MediatR types in the shared kernel, you've gone too far.

```
SharedKernel/
  Domain/
    Entity.cs
    AggregateRoot.cs
    IDomainEvent.cs
    IUnitOfWork.cs
  Results/
    Result.cs
    Error.cs
  Events/
    IIntegrationEvent.cs
    OrderPlacedIntegrationEvent.cs
    PaymentCompletedIntegrationEvent.cs
  Ids/
    CustomerId.cs
    ProductId.cs
```

## Governance

The shared kernel is shared code, which means changes to it affect all modules. You need governance:

1. **Code review required** - Any change to the shared kernel must be reviewed by representatives of all consuming modules
2. **Backward compatibility** - Don't remove or rename types. Add new ones instead
3. **Small surface area** - Resist the urge to add convenience methods. Keep it minimal
4. **Versioning** - In a monorepo this is less critical, but treat the shared kernel as a contract

```csharp
// BAD: Adding a helper because it's convenient
public static class StringExtensions
{
    public static string ToSlug(this string input) =>
        input.Trim().ToLowerInvariant().Replace(' ', '-');
}

// GOOD: Adding a type that genuinely defines a cross-module contract
public interface IIntegrationEventHandler<in TEvent>
    where TEvent : IIntegrationEvent
{
    Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default);
}
```

## Registering Shared Kernel Services

If the shared kernel provides services (like a [**domain event dispatcher**](https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems)), register them in a single extension method:

```csharp
public static class SharedKernelServiceExtensions
{
    public static IServiceCollection AddSharedKernel(
        this IServiceCollection services)
    {
        services.AddScoped<IDomainEventDispatcher, DomainEventDispatcher>();

        return services;
    }
}

// In Program.cs
builder.Services.AddSharedKernel();
builder.Services.AddCatalogModule();
builder.Services.AddOrderingModule();
```

Keep the zero-dependency rule intact: the shared kernel defines `IDomainEventDispatcher`, but the implementation and this registration extension live in a shared *infrastructure* project (which is allowed to reference `Microsoft.Extensions.DependencyInjection.Abstractions`).

## Summary

1. The shared kernel is a small, explicit set of shared types - not a dumping ground for convenience code.
2. Include base domain types, common interfaces, the result pattern, integration event contracts, and shared IDs.
3. Exclude module-specific logic, infrastructure, utilities, and DTOs.
4. The shared kernel project should have zero external NuGet dependencies.
5. Require code reviews for any shared kernel change since it affects all modules.
6. Keep the surface area as small as possible - when in doubt, don't share it.

Thanks for reading, and stay awesome!

---

## Frequently asked questions

### What is the shared kernel pattern?

The shared kernel is a small, explicitly agreed-upon set of code that multiple bounded contexts or modules share. In a .NET modular monolith it is typically one project with base domain types, common interfaces, and the result pattern.

### What belongs in a shared kernel?

Base entity and aggregate root classes, domain event interfaces, the Result and Error types, and strongly typed IDs that cross module boundaries. Everything in it should be needed by at least two modules.

### What should not go in a shared kernel?

Module-specific domain logic, infrastructure code, DTOs, and convenience utilities. If removing a type would only break one module, it does not belong in the shared kernel.

### Should the shared kernel have NuGet dependencies?

No. Keep it pure domain code with zero external dependencies. If you feel the need to reference EF Core or MediatR from the shared kernel, that code belongs in a shared infrastructure project instead.

### Is a shared kernel the same as a Common project?

No. A Common project tends to accumulate anything two developers wanted to reuse. A shared kernel is deliberately minimal and governed: every addition is reviewed because it creates coupling across all modules.
