FARFETCH — modernizing a live e-commerce app
Problem
FARFETCH is a luxury fashion marketplace, and its Android app takes orders from customers around the world. By 2021 the codebase carried years of accumulated decisions: MVC in the oldest screens, MVVM in the newer ones, dependencies wired by hand throughout. None of it was wrong when it was written, and all of it made the next change slower than the last.
The continuous integration pipeline took fifteen minutes per run. Fifteen minutes is past the threshold where engineers stop running it and start batching, which is how integration problems get found late and expensively.
Constraint
Nothing could stop. The app was in production taking revenue, on a release train that did not pause for architecture work. Every change had to land incrementally, and both the old and the new patterns had to stay alive in the same codebase for as long as the migration ran — which meant years, not sprints.
Decisions
- Jetpack Compose adopted screen by screen, behind the existing navigation. New screens were written in Compose; old ones moved when they were being opened for other reasons anyway. No migration sprint, no feature freeze, no branch that lives for six months.
- Inversion of control through dependency injection. This was the change that made the rest possible. Untangling construction from behaviour is what let a single screen move without dragging its dependencies with it.
- Modularization first, caching second — in that order. Splitting the monolith into Gradle modules is what made the build cacheable at all. With a single module almost any change invalidates the whole build, so a cache buys you very little; once the boundaries exist, a change touches a few modules and the rest are served from cache.
- Then both Gradle caches, against two different costs. The build cache reuses task outputs across builds and across machines, so CI stops recomputing what another run already produced. The configuration cache removes the configuration phase from repeat runs — the fixed price every single build paid before, regardless of how little had changed.
Result
The pipeline went from 15 minutes to 2.5 on average — back under the threshold where engineers run it per change instead of batching.
Compose and dependency injection both became the default for new work rather than parallel experiments, which is the only durable outcome an incremental migration can have: the new way has to be the easy way, or the old way wins by inertia.
What I'd do differently
I would modularize before migrating, not alongside it. We did both at once, and the same argument I make for the build cache applies to the migration: module boundaries are what make an incremental change cheap. Without them, every screen moved to Compose still dragged the whole dependency graph behind it, and the migration paid a tax it did not need to pay. The pipeline work proved the point — I just proved it in the wrong order.