Main-safe Isn't a Threading Rule — It's the Contract That Makes Android Layers Real
A codebase without main-safe forces every ViewModel and UseCase to understand the threading model of every dependency. That knowledge spreads fast, makes refactors dangerous, and turns the UI layer into a fragile dispatch center. Main-safe draws a hard line that keeps the UI layer testable, swappable, and ignorant of infrastructure — the same property that makes Clean Architecture and hexagonal ports work.
Main-safe is widely misunderstood as just "Repository switches threads so the UI doesn't have to." The real shift is that complexity stops leaking upward. In a main-safe architecture, the UI layer expresses intent and displays state; it never sees Dispatchers.IO, never guesses whether a call is heavy, and never becomes a dispatch center for threading decisions. The Data layer absorbs all of that — IO, concurrency, scheduling, data-source switching — and presents a clean suspend-fun surface.
The contract rests on four "no leaks": no leakage of the threading model, no leakage of blocking risk, no leakage of concurrency semantics, and no leakage of call cost. When any of those leaks, the boundary between layers dissolves and the UI starts making execution decisions it has no business making.
Achieving main-safe fixes thread spillover but doesn't fix business-logic spillover. The next trap is a Repository that starts making business decisions — caching conditionally, transforming data based on rules — which turns the Data layer into a god object. The fix is pulling those decisions into UseCases, keeping the Repository a pure data-fetching surface. That progression — thread safety, then responsibility safety, then structural safety — is what separates a genuine layered architecture from one that just looks layered on a diagram.
Most Android teams treat main-safe as a coroutine best practice, but it's actually a test of whether the architecture has a real boundary. If a ViewModel ever imports Dispatchers, the boundary has already failed.
The four 'no leaks' reframe main-safe from a positive rule ('use withContext in the Repository') to a negative constraint ('the UI must never see these concepts'). Negative constraints are easier to enforce in code review because violations are unambiguous.
Business-logic spillover is the natural next failure mode after thread spillover is fixed. A Repository that conditionally caches or transforms data is doing the UseCase's job, and the symptom is the same as thread leakage: the caller starts depending on implementation details.
The three-level maturity model exposes a common pattern: teams stop at Level 1 and declare victory, then wonder why their codebase still feels coupled. Thread safety is necessary but nowhere near sufficient.
Calling main-safe a 'watershed' is accurate because it's a one-way door. Once a codebase commits to main-safe, the UI layer's relationship to infrastructure changes permanently — you can't accidentally reintroduce Dispatchers.IO into a ViewModel without it sticking out as a violation.