ISSUE 42
Programming & Languages

Interface Pollution and Package Boundaries in Go Microservices: Designing for Seam Clarity

An exploration of how over-abstracting with interfaces in Go microservices leads to brittle codebases, and how to define effective package boundaries to maintain modularity.

Abhik Kumar Panda
Abhik Kumar Panda
Creator & Engineer
August 24, 2026 · 4 min read
Abstract representation of Go package boundaries and interface connections in a distributed architecture.

In the Go ecosystem, the mantra ‘accept interfaces, return structs’ is often misinterpreted as a mandate to abstract every single dependency behind an interface. While interfaces are a powerful tool for decoupling, indiscriminate application frequently results in ‘interface pollution’—a state where the codebase is littered with thin, single-implementation interfaces that provide no tangible benefit while significantly increasing cognitive load.

This pattern often stems from a cargo-culting of Java-style dependency injection, where engineers feel compelled to mock every external interaction. In Go, this approach obscures the actual flow of data and makes navigating the codebase through IDE tools like ‘Go to Definition’ frustratingly difficult.

The Anatomy of Interface Pollution

Interface pollution occurs when an interface exists solely to facilitate testing, rather than to define a clear architectural boundary. If a package defines an interface for a service that is never implemented by another type, you have likely introduced an unnecessary abstraction. This layer adds indirection without providing flexibility, effectively shielding the implementation details from the caller at the cost of clarity.

// Anti-pattern: The 'Interface-per-Service' trap
type UserRepository interface {
    GetUser(id string) (*User, error)
    CreateUser(u *User) error
}

type userService struct {
    repo UserRepository
}

// If there is only one implementation, this interface is likely noise.

When we define interfaces too early, we often create ‘leaky’ abstractions. The interface might expose methods that are only relevant to specific callers, or worse, it might force the caller to know about types that belong in the implementation layer. A better approach is to define interfaces at the point of consumption, ensuring they are as small as possible and satisfy the specific needs of the caller.

Designing Effective Package Boundaries

The root cause of over-abstracting is often poorly defined package boundaries. In a microservices architecture, packages should be organized by domain capability rather than by technical layer. When packages are too small or granular, developers feel the need to use interfaces to bridge the gaps between them, leading to a ‘spaghetti’ of interfaces connecting tiny packages.

Instead, group related code into cohesive packages that represent a single domain concept. If two components are tightly coupled and constantly interact, they might belong in the same package. By keeping related logic together, you eliminate the need for interfaces to cross package boundaries, allowing for direct calls that are easier to trace and refactor.

The Role of Domain-Driven Design

Domain-Driven Design (DDD) provides a framework for defining these boundaries. By identifying ‘Bounded Contexts’, you can ensure that your package structure reflects the business logic. Within a bounded context, you should favor concrete types. Interfaces should be reserved for the edges of your service—where your domain logic interacts with infrastructure concerns like databases, message queues, or external APIs.

Testing Without Over-Abstraction

The most common argument for interface pollution is the need for unit testing. However, Go’s testing tools are powerful enough to handle integration testing with minimal overhead. Rather than mocking every database call, consider using test containers or local instances of your dependencies. This validates that your code works against the actual infrastructure, which is often more valuable than a mock that might pass even when the real system fails.

If your tests are the only reason an interface exists, reconsider whether you are testing the right thing.

Conclusion

Maintaining a clean Go codebase requires a disciplined approach to abstraction. Interfaces should be used to define boundaries between disparate concerns—such as the boundary between your business logic and the persistence layer—not as a default for every component. By focusing on cohesive package boundaries and prioritizing concrete types, you can reduce indirection and build microservices that are easier to understand, test, and maintain.

Ultimately, the goal is to make the codebase ‘self-documenting’ through clear, direct calls. When you remove unnecessary interfaces, the structural intent of your architecture becomes visible, allowing engineers to focus on business value rather than navigating layers of abstraction.

Share Twitter LinkedIn
Abhik Kumar Panda
CONTRIBUTING FELLOW

Abhik Kumar Panda

Creator & Engineer

Software engineer and creator passionate about technical writing, systems architecture, and AI.

Continue Reading

Stay in the loop.

Get new engineering articles, systems ideas, and things worth knowing — straight to your inbox.

No spam. Just useful things.
Unsubscribe anytime.