software development
interfaces
software architecture
coding best practices
.NET

Should I have a separate assembly for interfaces?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

A separate assembly for interfaces can be useful, but it is not automatically good architecture. The practical answer is: create a separate contract assembly only when those interfaces form a real shared boundary between independently evolving parts of the system; otherwise, keeping interfaces close to the code that consumes them is usually simpler and healthier.

When a Separate Interface Assembly Makes Sense

A dedicated interface assembly is justified when the abstractions are shared across project boundaries and you want multiple implementations or consumers to depend on the same stable contract.

Common examples include:

  • plugin systems
  • SDK-style public contracts
  • cross-team service boundaries
  • shared abstractions used by more than one implementation package

In those cases, a contract assembly can reduce coupling by preventing consumers from referencing a heavyweight implementation assembly.

When It Is Over-Engineering

Many codebases create an interface assembly too early just because "interfaces are good." That often produces extra projects, extra versioning, and extra indirection without a real payoff.

If the interface is used only inside one application layer and has no independent reuse or boundary role, it is often better to keep it near the consuming code.

That is especially true for small or medium applications where every extra assembly increases solution complexity, build surface area, and package coordination.

A Good Rule: Put Abstractions Near the Consumer

A useful design rule is that abstractions usually belong with the code that depends on them, not with the code that implements them.

For example, if an application service depends on IClock, IEmailSender, or IOrderRepository, those interfaces often belong in the application layer because that is where the dependency direction points.

The infrastructure layer then references that assembly and implements the interfaces.

A Simple Example

Suppose you have these projects:

  • 'MyApp.Application'
  • 'MyApp.Infrastructure'
  • 'MyApp.Web'

A reasonable shape is:

  • 'MyApp.Application defines IOrderRepository'
  • 'MyApp.Infrastructure implements SqlOrderRepository'
  • 'MyApp.Web depends on the application layer, not directly on SQL details'

A tiny example interface and implementation:

csharp
1namespace MyApp.Application;
2
3public interface IOrderRepository
4{
5    Task<Order?> GetByIdAsync(Guid id);
6}
csharp
1using MyApp.Application;
2
3namespace MyApp.Infrastructure;
4
5public sealed class SqlOrderRepository : IOrderRepository
6{
7    public Task<Order?> GetByIdAsync(Guid id)
8    {
9        // Query the database here.
10        return Task.FromResult<Order?>(null);
11    }
12}

This gives you abstraction without needing a separate MyApp.Interfaces assembly.

When a Separate Contracts Assembly Is Worth It

A separate assembly becomes more defensible when the contracts have a real life of their own.

For example, in a plugin model you may genuinely want:

  • 'MyProduct.Abstractions'
  • 'MyProduct.PluginA'
  • 'MyProduct.PluginB'

In that shape, a dedicated abstraction assembly is the natural dependency hub.

The important point is that the extra assembly should represent a meaningful architectural boundary, not merely a habit.

Versioning and Ownership Matter

A contract assembly creates a versioned public surface. That can be helpful when several teams or packages consume it, but it also means interface changes become more operationally expensive.

So before splitting interfaces into their own assembly, ask:

  • who owns these contracts
  • who consumes them
  • how often they change
  • does independent versioning help or hurt

If the answer is "one team, one app, frequent changes," a separate assembly is often a cost, not a benefit.

Common Pitfalls

Creating a global Interfaces project for every abstraction in the system is a common smell. It usually centralizes unrelated contracts that do not belong together.

Putting interfaces near implementations rather than near consumers can also invert dependency direction in awkward ways.

Another mistake is assuming interfaces are valuable even when there will only ever be one implementation and no meaningful boundary to preserve.

Finally, every new assembly adds build, packaging, and version-management overhead. Count that cost honestly.

Summary

  • a separate assembly for interfaces is useful only when the interfaces represent a real shared contract boundary
  • in many applications, interfaces belong with the consuming layer instead
  • contract assemblies are a good fit for plugins, SDKs, and independently reused abstractions
  • creating a generic Interfaces project by default is usually over-engineering
  • let dependency direction, ownership, and versioning needs drive the decision rather than habit

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.