The Infrastructure Layer in Clean Architecture

The Infrastructure Layer in Clean Architecture

5 min read··

clean-architecturedotnetef-core

The Infrastructure layer is where everything that talks to the outside world lives: databases, message brokers, email providers, caches. It implements the interfaces the inner layers define, and it is the only place EF Core should ever appear.

The Infrastructure layer gets the least attention in Clean Architecture discussions, yet it's where most of the actual code ends up. Here is how to structure it in .NET so the frameworks stay out of your domain.

What Is the Infrastructure Layer?

The Infrastructure layer is the outermost layer in Clean Architecture. It provides implementations for the abstractions defined in the Domain and Application layers.

Everything that talks to the outside world lives here:

  • Database access - EF Core DbContext, Dapper queries, repository implementations
  • External API clients - payment gateways, email services, third-party APIs
  • Message brokers - RabbitMQ, Azure Service Bus, Kafka
  • File storage - local disk, Azure Blob Storage, S3
  • Caching - Redis, in-memory cache
  • Authentication providers - Identity, OAuth, JWT token services

The Infrastructure layer depends on the Domain and Application layers. It implements their interfaces. The inner layers never reference Infrastructure directly.

Dependency arrows pointing inward: Infrastructure implements the interfaces of Application, which depends on Domain, so the inner layers never reference Infrastructure

Implementing Repository Interfaces

The Domain layer defines repository interfaces (some teams keep them in the Application layer instead; either placement works). Infrastructure provides the implementation:

// Domain layer - the interface
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    void Add(Order order);
}
// Infrastructure layer - the implementation
public sealed class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _dbContext;

    public OrderRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Order?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext.Orders
            .Include(o => o.LineItems)
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public void Add(Order order)
    {
        _dbContext.Orders.Add(order);
    }
}

The repository pattern keeps EF Core confined to the Infrastructure layer. Your domain logic never sees a DbContext.

The DbContext

Your DbContext lives in Infrastructure and handles all EF Core configuration:

public sealed class AppDbContext : DbContext, IUnitOfWork
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(AppDbContext).Assembly);
    }
}

Entity configurations use IEntityTypeConfiguration<T>:

public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasKey(o => o.Id);

        builder.Property(o => o.Status)
            .HasConversion(
                status => status.Name,
                name => OrderStatus.FromName(name));

        builder.HasMany(o => o.LineItems)
            .WithOne()
            .HasForeignKey(li => li.OrderId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.ComplexProperty(o => o.TotalAmount, money =>
        {
            money.Property(m => m.Amount).HasColumnName("total_amount");
            money.Property(m => m.Currency).HasColumnName("total_currency");
        });
    }
}

OrderStatus is a smart enum from the Domain layer, a class rather than a plain enum, so we tell EF Core explicitly how to convert it: store the Name, rehydrate with the FromName lookup on the Enumeration<TEnum> base class.

Implementing External Service Interfaces

The Application layer defines interfaces for external services. Infrastructure implements them:

// Application layer
public interface IEmailService
{
    Task SendOrderConfirmationAsync(
        string recipientEmail,
        Guid orderId,
        CancellationToken cancellationToken = default);
}
// Infrastructure layer
public sealed class EmailSettings
{
    public string BaseUrl { get; init; } = string.Empty;
    public string OrderConfirmationTemplateId { get; init; } = string.Empty;
}

public sealed class EmailService : IEmailService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly EmailSettings _settings;

    public EmailService(
        IHttpClientFactory httpClientFactory,
        IOptions<EmailSettings> settings)
    {
        _httpClientFactory = httpClientFactory;
        _settings = settings.Value;
    }

    public async Task SendOrderConfirmationAsync(
        string recipientEmail,
        Guid orderId,
        CancellationToken cancellationToken = default)
    {
        var client = _httpClientFactory.CreateClient("EmailApi");

        var request = new SendEmailRequest
        {
            To = recipientEmail,
            Subject = "Order Confirmed",
            TemplateId = _settings.OrderConfirmationTemplateId,
            Data = new { OrderId = orderId }
        };

        await client.PostAsJsonAsync("/v1/emails", request, cancellationToken);
    }
}

Implementing Caching

Application layer defines the caching abstraction:

// Application layer
public interface ICacheService
{
    Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
    Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default);
    Task RemoveAsync(string key, CancellationToken cancellationToken = default);
}
// Infrastructure layer
public sealed class RedisCacheService : ICacheService
{
    private readonly IDistributedCache _cache;

    public RedisCacheService(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task<T?> GetAsync<T>(
        string key,
        CancellationToken cancellationToken = default)
    {
        var bytes = await _cache.GetAsync(key, cancellationToken);

        return bytes is null
            ? default
            : JsonSerializer.Deserialize<T>(bytes);
    }

    public async Task SetAsync<T>(
        string key,
        T value,
        TimeSpan? expiration = null,
        CancellationToken cancellationToken = default)
    {
        var bytes = JsonSerializer.SerializeToUtf8Bytes(value);

        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(5)
        };

        await _cache.SetAsync(key, bytes, options, cancellationToken);
    }

    public async Task RemoveAsync(
        string key,
        CancellationToken cancellationToken = default)
    {
        await _cache.RemoveAsync(key, cancellationToken);
    }
}

DI Registration Module

Keep your Infrastructure DI registrations organized in a single method:

public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddDbContext<AppDbContext>(options =>
            options.UseNpgsql(configuration.GetConnectionString("Database")));

        services.AddScoped<IUnitOfWork>(sp =>
            sp.GetRequiredService<AppDbContext>());

        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<ICustomerRepository, CustomerRepository>();

        services.Configure<EmailSettings>(configuration.GetSection("Email"));

        services.AddHttpClient("EmailApi", client =>
            client.BaseAddress = new Uri(configuration["Email:BaseUrl"]!));

        services.AddScoped<IEmailService, EmailService>();
        services.AddScoped<ICacheService, RedisCacheService>();

        services.AddStackExchangeRedisCache(options =>
            options.Configuration = configuration.GetConnectionString("Redis"));

        return services;
    }
}

Then in Program.cs:

builder.Services.AddInfrastructure(builder.Configuration);

This keeps the Presentation layer from knowing about Infrastructure internals.

Folder Structure

Infrastructure/
  Data/
    AppDbContext.cs
    Configurations/
      OrderConfiguration.cs
      CustomerConfiguration.cs
    Repositories/
      OrderRepository.cs
      CustomerRepository.cs
    Interceptors/
      PublishDomainEventsInterceptor.cs
  Services/
    EmailService.cs
    PaymentService.cs
  Caching/
    RedisCacheService.cs
  Messaging/
    EventBus.cs
    Consumers/
      OrderCompletedConsumer.cs
  DependencyInjection.cs

Common Mistakes

1. Leaking Infrastructure into the Domain. If your entity has [Column] or [Table] attributes, you've coupled the Domain to EF Core. Use IEntityTypeConfiguration<T> instead.

2. Not using IEntityTypeConfiguration<T>. Putting all configuration in OnModelCreating becomes unmanageable. One config class per entity.

3. Referencing Infrastructure from Application. The Application layer should only use interfaces. If you see using Infrastructure; in an Application class, something is wrong.

4. Missing the Unit of Work. Don't call SaveChanges in every repository method. Use a dedicated Unit of Work that commits after the use case completes.

5. Fat Infrastructure classes. If your email service also handles templates, formatting, and retry logic, split it. Keep each implementation focused.

Summary

The Infrastructure layer is where all external concerns live. It implements the interfaces defined by inner layers and keeps framework dependencies from leaking inward.

Structure it by concern (Data, Services, Caching, Messaging), register everything cleanly, and test it with integration tests that verify actual external interactions.

Thanks for reading, and stay awesome!


Frequently Asked Questions

What goes in the infrastructure layer in Clean Architecture?

Everything that talks to the outside world: the EF Core DbContext and repository implementations, external API clients, message broker integration, file storage, caching, and authentication providers. It implements the interfaces defined by the Domain and Application layers.

Does the infrastructure layer depend on the application layer?

Yes. Infrastructure references Application (and Domain transitively) so it can implement their interfaces. The inner layers never reference Infrastructure; the DI container connects them at runtime.

Where does the DbContext belong in Clean Architecture?

In the infrastructure layer, along with entity configurations and repository implementations. The application layer interacts with persistence only through abstractions like repositories and a unit of work.

Should each repository call SaveChanges?

No. Repositories only stage changes. A unit of work, typically the DbContext itself behind an IUnitOfWork interface, commits once after the use case completes, so a single use case produces a single transaction.

How do you test the infrastructure layer?

With integration tests against real dependencies. Testcontainers can spin up throwaway Docker containers for PostgreSQL, Redis, or RabbitMQ, so your tests exercise real EF Core queries instead of mocks.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.