Prevention Over Cure: Enforcing Android Architecture Through Contracts, Not Code Review
Android projects that rely on code review to catch architectural violations inevitably accumulate invisible coupling: callers can't know if a function is main-safe, exceptions have no clear owner, and Flow lifecycles become a shared mystery. Enforcing these boundaries through contracts rather than vigilance stops complexity from leaking into layers where it becomes expensive to remove.
Most Android teams rely on governance—lint rules, refactors, and code review—to manage complexity, but this approach only limits damage after the fact. The real failure happens when thread dispatchers leak into ViewModels, repositories force callers to manage IO, and exception handling fragments across every layer. These patterns create systems where no one dares to remove a `withContext(IO)` call because the consequences are unknowable.
A preventive architecture instead enforces contracts at the design level. Repositories guarantee main-safety internally, exceptions are converged into a sealed `Result` type at the data layer, and UseCases lock down business composition so ViewModels only handle state mapping. The goal is complexity convergence: thread management stays in the data layer, business logic stays in the domain layer, and UI state aggregates in a single ViewModel `StateFlow`.
The trade-off is upfront cost—writing UseCases and Result wrappers even for simple features feels like over-engineering. But that cost hedges against the far larger expense of untangling a codebase where thread responsibility, error ownership, and Flow lifecycles have become collective unknowns.
The argument reframes architecture not as a set of coding rules but as a system of enforceable contracts that make incorrect usage structurally difficult—a shift from policing developers to designing APIs that resist misuse.
Describing governance as 'firefighting' captures why lint rules and code review scale poorly: they detect symptoms after the fact but never prevent the initial misplacement of responsibility.
The observation that no one dares delete a `withContext(IO)` call in a mature codebase is a concrete litmus test for whether thread responsibility has already leaked beyond the data layer.
Requiring UseCases even for simple features is controversial in teams that prioritize velocity, but the piece frames this as a calculated trade-off where the real cost is deferred complexity that compounds silently.