Erasmus Labs Notes

Mobile architecture in AI era

MVVM was the right answer while humans typed the boilerplate. Agents type it now - so my solo apps run the strict architectures built for hundred-engineer teams. Receipts from Era and Lekton.

The ideas here are mine - AI helps edit and sharpen the writing, and I do the final pass.

Every mobile engineer has heard some version of the same interview question: are you going with MVC or MVVM for this? The expected answer has been pretty stable for a decade - MVVM gives you enough structure to grow without taking on too much overhead, maybe even MVC for something small. I’ve asked versions of this question hundreds of times myself.

The reasoning was always team size. I’ve been doing mobile for a bit over ten years - a five-person startup, a 200-person company, now Uber - and each team needs a different amount of architecture. Structure costs engineering time, and you don’t want to pay for more structure than your team needs.

Uber runs on RIBs, which we open-sourced. It’s designed for apps with a large number of engineers, splitting logic into small units so hundreds of engineers can work in the same codebase without constantly stepping on each other. We had just finished migrating from MVVM to RIBs when I joined Uber in 2018. Put that architecture on a ten-person team, though, and you can end up spending a lot of time wiring Routers, Interactors and Builders instead of shipping.

That calculation assumes a human has to write all the boilerplate and plumbing. Coding agents change that pretty significantly. I now ship Erasmus Labs projects essentially solo, with multiple agents working in parallel. The cost of architecture boilerplate is close to zero. In fact, for my personal projects I barely read most of the implementation code anymore. I mostly review the architecture, important state transitions, and behavior.

And I’ve found something I didn’t expect: strict architecture is actually more useful when agents are writing the code.

Every app I ship now uses The Composable Architecture on iOS and Mac, and MVI on Android - Era on both platforms, Lekton, and a few more in progress. There are around 90 reducers across everything I’m currently working on. The two aren’t identical, but they share the same basic idea: state flows down, events flow up, and state changes go through a defined path.

For example, when a kid opens a book from their profile in Era, the profile doesn’t reach into the reader and change it directly. It emits .delegate(.openBook) and the parent decides what that means. Era’s iOS app has 29 of these delegate messages across 14 features; Android has a similar setup with a sealed Contracts file per feature.

Up the pipe, back down as state - the direct path between siblings doesn’t exist.

This is particularly useful with agents. When an agent is working on a feature, most of the relevant context is that feature’s state, actions and reducer. It doesn’t need to understand the other 40 files in the app. And because state changes have to go through the reducer, there are fewer hidden side effects for an agent to accidentally miss.

The biggest change for me, though, is testing. With TCA, a user flow can be represented as a sequence of actions and state changes. I can build a TestStore, send those actions, and assert every state transition without launching the UI or driving UI automation.

Lekton is my cleanest example. It’s a dictation app with eight features, 638 tests, including 115 full headless TestStore flows. That’s about 10,000 lines of test code against 20,000 lines of app code. For a solo developer, writing all of that manually would have been hard to justify.

The obvious downside is verbosity. Adding a feature to TCA means declaring state, actions and reducer wiring before you render the first view. If a child needs to communicate something to a distant parent, the action has to travel through the layers in between. Ten years ago, that overhead could easily outweigh the benefit.

Now the plumbing is the agent’s problem. I mostly review the seams - which delegate actions exist, what they mean, and where state lives - rather than spending my time reviewing every piece of wiring between them.

This also changes how I think about the old “less code is better” argument. The real cost was never lines of code. It was how much code you had to understand before safely making a change.

For an LLM, that’s even more important. The relevant metric is context, not code size. An app with a tenth of the code isn’t necessarily easier if the agent has to understand the whole codebase to make a change. An app with ten times the code can be easier if the agent only needs to read one feature and its immediate dependencies.

A small codebase with all twelve blocks highlighted as required context, next to a codebase ten times larger where only a handful of blocks are highlighted

Ten times the code can still be the easier codebase to change.

This brings me back to RIBs, because it got part of this right. Its compartmentalization is exactly what you want when you need to limit how much of a codebase someone has to understand. But communication still involves following dependencies, listeners and streams through the tree, so an agent investigating a change may have to chase those relationships to understand the full effect.

RIBs was built to protect hundreds of engineers from each other. I’m finding that strict reducer architectures are useful for protecting the product from my agents.

So maybe the interview answer needs an update. Now, you can afford more structure because the part that used to consume engineering time - the repetitive plumbing - is increasingly something you can delegate. I’d rather have the agent pay the boilerplate tax and spend my time reviewing the boundaries and behavior.

Discuss on X.