Opening

Layers are the most fundamental concept in software architecture. Every architecture you have ever seen is basically a set of layers with rules about how they communicate. UI layer talks to domain layer, domain layer talks to data layer. Dependencies point inward. It sounds clean and elegant. But today I want to ask a question that does not get asked enough: when do layers actually help, and when are they just overhead you are paying for nothing? Because here is the reality — layers exist to manage change. And if your layers never change independently, you are paying a tax for insurance you will never collect on.

Why Layers Exist

Let us start with the fundamental reason layers exist. They manage change. When your API response format changes, you want to update your data layer without touching your UI. When your design team redesigns a screen, you want to update your UI without touching your business logic. Layers create boundaries, and boundaries contain the blast radius of change. If your API returns a new field, ideally you update one mapper and one model in your data layer, and everything above it is unaffected. That is the promise. And when it works, it is genuinely powerful. On large codebases with multiple teams, layers prevent one team's changes from breaking another team's code.

The Dependency Rule

At the heart of layered architecture is the dependency rule. Inner layers must not know about outer layers. Your domain layer must not import anything from your data layer or your presentation layer. Dependencies always point inward. This is the rule that makes layers actually useful. If your domain model imported Retrofit annotations, then changing your network library would require changing your domain model, and the whole point of the layer is defeated. The dependency rule is enforced through interfaces. Your domain layer defines a Repository interface. Your data layer provides the implementation. Your domain layer says "I need something that can give me a User." Your data layer says "I am that thing, and I get the User from this API." The domain does not know or care about the how.

When the Domain Layer Is a Pass-Through

Here is where things get real. On many Android apps, the domain layer looks like this. You have a GetUserUseCase that takes a UserRepository and calls getUser. You have a GetPostsUseCase that takes a PostRepository and calls getPosts. You have a GetCommentsUseCase that takes a CommentRepository and calls getComments. No transformation. No business logic. No orchestration. Just delegation. When your domain layer is nothing but a relay station passing messages from the presentation layer to the data layer without adding value, it is not a layer. It is overhead. You have added a directory, a set of classes, and a set of interfaces that serve no purpose other than satisfying an architectural diagram. My advice? If your domain layer is a pass-through, delete it. Let your ViewModel talk directly to your Repository. You can always add a domain layer later when you actually have domain logic to put there.

Leaky Abstractions

Joel Spolsky coined the term "leaky abstractions" and it perfectly describes what happens with poorly designed layers. The Law of Leaky Abstractions says: all non-trivial abstractions, to some degree, are leaky. Your Repository interface says it returns a Flow of User. But the behavior of that Flow — whether it emits once or continuously, whether it retries on failure, how it handles caching — leaks through the interface. Your ViewModel has to know these behaviors to use the Repository correctly. The interface did not actually abstract the complexity; it just hid the implementation behind a door you have to peek through anyway. This is especially common with error handling. Your Repository interface returns Result of User. But is it a network error? A parsing error? A timeout? The presentation layer needs to distinguish these to show the right error message, which means the data layer's concerns are leaking right through the supposed boundary.

The One-Implementation Interface

Here is a pattern I see constantly that drives me slightly crazy. An interface with exactly one implementation. UserRepository interface with getUserById. UserRepositoryImpl that implements UserRepository. That is it. One interface, one implementation, forever. "But we might need a second implementation someday." No, you will not. And if you do, you can extract the interface in about thirty seconds with your IDE. An interface with one implementation is not abstraction — it is indirection. It doubles the number of files you navigate. It makes "go to definition" take you to the interface instead of the code. It creates noise in your project structure. The one legitimate reason for a single-implementation interface is testing. If you inject the interface, you can provide a fake in tests. But with modern tools like MockK or Turbine, you can mock concrete classes just fine. So even the testing argument is weaker than it used to be.

What Google Actually Does

It is instructive to look at what Google's own teams do. The Now in Android sample — which is Google's official reference architecture — uses a simplified layering approach. ViewModels depend directly on Repository classes. There are no use cases by default. Use cases appear only when there is actual logic to encapsulate, like combining data from multiple repositories. Models are shared across layers when they have the same shape. Repository interfaces exist primarily because multiple data sources make the abstraction genuinely useful. Compare this to what you see in tutorials: rigid three-layer architecture, mandatory use cases for every operation, separate models at every boundary. The gap between what Google recommends and what the community practices is surprisingly large.

When Layers Help: Real Criteria

So let me give you concrete criteria for when layers earn their keep. A layer is worthwhile when it changes independently from the layers around it. If your UI changes frequently but your data sources are stable, the boundary between presentation and data is valuable. A layer is worthwhile when it contains non-trivial logic. If your domain layer orchestrates calls to three repositories, merges the results, applies business rules, and transforms the output, that is a real layer with real value. A layer is worthwhile when different teams own different layers. The boundary becomes a contract that teams agree on, reducing coordination overhead. And a layer is worthwhile when you have multiple implementations. If your Repository has a real implementation and an offline-first implementation, the interface is doing real work.

When Layers Are Overhead

Conversely, a layer is overhead when it never changes independently. If every UI change also requires a domain change and a data change, your layers are not containing change — they are multiplying it. A layer is overhead when it contains no logic. If every method is a one-line delegation to the next layer, it is a pass-through adding nothing. A layer is overhead when one person or a small team owns the entire codebase. You do not need contractual boundaries with yourself. And a layer is overhead when the abstraction leaks so badly that the layer above needs intimate knowledge of the layer below anyway. In that case, the boundary is fiction.

Pragmatic Layering in Practice

Here is what pragmatic layering looks like. Start with two layers: presentation and data. Your ViewModel handles UI logic. Your Repository handles data fetching, caching, and coordination. That is your starting point. When your ViewModel starts doing things like combining data from multiple repositories and applying business rules, extract that logic into a use case or domain service. Now you have three layers, but only where the complexity justified it. Use interfaces where you have or genuinely expect multiple implementations. Use concrete classes everywhere else. Share models across boundaries when they have the same shape. Split them when they diverge. This is not lazy architecture. This is architecture that earns its complexity.

The Litmus Test

I want to give you a simple litmus test you can apply to any layer in your codebase. Ask yourself: if I deleted this layer and moved its responsibilities to the layer above or below, would I lose anything? If the answer is no — if deleting the layer changes nothing except reducing the number of files — it is overhead. Delete it. If the answer is yes — you would lose testability, or you would couple things that should be separate, or you would force a team to depend on another team's internal details — then the layer is earning its keep. Keep it. Apply this test ruthlessly. Your codebase will thank you.

Closing

Here is what I want you to take away. Layers are not inherently good or bad. They are a tool for managing change and complexity. Use them where change actually happens independently. Remove them where they are just adding files without adding value. Look at what your layers actually contain. If they contain logic, keep them. If they contain ceremony, question them. And remember: the goal is not architectural purity. The goal is a codebase that your team can understand, modify, and ship with confidence. Sometimes that means three layers. Sometimes that means two. Sometimes it means five in one part of the app and two in another. Next up, we are going to tackle state management — MVI versus MVVM versus MVC and the real trade-offs at runtime.