Most iOS architectures do not collapse in one dramatic moment. They drift.
It usually starts with something reasonable. A screen needs data, so the ViewModel reaches for a service. A deadline is tight, so a service updates state directly because it is already holding the result. A bit of async work runs on the main actor because it fixes a flicker. Everyone moves on.
Nothing breaks. Shipping continues. The choices even look pragmatic, because each one is small.
Then, slowly, the system begins to push back.
A new feature is not just a new feature anymore. It touches three areas that have no obvious relationship. Tests that used to be lightweight now need half the app to be alive. A refactor becomes a negotiation, not because the code is scary, but because ownership is unclear. Who is allowed to mutate state, and where are side effects supposed to live?
That question is the beginning of the real problem.
When boundaries blur, you get coupling that hides in plain sight. State stops being “state” and starts collecting reference objects, caches, view contexts, and orchestration helpers. Objects that used to be independent now know about each other’s internals. Dependency injection feels explicit, but the graph deepens anyway. A type that once had two dependencies ends up with eight, and nobody is confident they can remove any of them.
The cost is rarely a single outage. It shows up as friction.
Engineers avoid certain files because every change produces surprises. Reviews take longer because intent is buried under wiring. Release days feel tense even when the team is talented, because the system is no longer predictable.
At that point, the usual move is to add more structure. A new pattern. A stronger container. Another layer to “organize” the layers you already have.
Sometimes that helps, briefly. More often it just adds surface area on top of a deeper issue. You can standardize the shapes of the pieces, but you still have not answered the hard question: who owns mutation, and who owns work?
Once that separation becomes explicit, the architecture tends to get smaller, not bigger.
State becomes value data only, the part that can be observed, copied, reasoned about, and tested without dragging the runtime behind it. Services do work and side effects, but they do not decide what the application becomes. Views render and emit intent through closures, and they stay out of storage, purchases, exports, and other “real world” concerns. Orchestration lives in one place, which means you can find it when something feels off.
Over time, those constraints stop feeling like rules and start feeling like relief. Dependency graphs flatten because you no longer need to inject everything everywhere. Independent components can exist without reaching into each other’s pockets. The codebase becomes easier to change because it becomes easier to understand.
In one project, this discipline eventually stabilized into a consistent structure. Internally, it ended up with a name: SCALES, short for Simple Composable Architecture Leveraging Events that Scales. The name is not the point. The point is what it protected: strict boundaries, minimal deep injection, explicit orchestration, and components that can communicate without entangling themselves.
The most noticeable shift is not technical. It is how the team feels inside the codebase.
When ownership of mutation is clear, hesitation drops. When services cannot quietly alter state, debugging becomes more direct. When views are reduced to rendering and intent, UI work stops pulling unrelated infrastructure into scope.
Architectures start fighting back when boundaries blur. They start cooperating again when responsibilities are narrowed and enforced.
The next step is to make those boundaries concrete, and to show how the pieces fit together without turning into ceremony.