Platform Engineering is Archaeology with Deadlines
I joined the company in 2019 to build a bank. Then COVID happened.
The banking project got shelved. People got reassigned. I ended up on a food delivery app that was absolutely exploding in growth. Turns out lockdowns are great for food delivery, terrible for experimental banking products.
My new team mentioned, almost as an aside, that theyâd been âmigrating the iOS app to Swiftâ for a couple years. Great! I love Swift. I could help with that.
I opened the codebase. 1.3 million lines of code. 750,000 lines still in Objective-C. Custom libraries that didnât map to modern Swift patterns. A modularization strategy that somehow resulted in most code living in a handful of Core modules. And a migration that had been âin progressâ since 2017.
The policy since 2018 was âall new code in Swift.â Sounds good in theory. In practice? Most of the core features (authentication, navigation, data management) were still in Objective-C. So engineers were either writing bridging code constantly or just giving up and writing new features in Objective-C too because it was easier than fighting the architecture.
This was going to be fun.

What I Signed Up For vs. What I Got
I thought I was joining a team that needed help rewriting code. What I actually walked into was a migration that had lost all momentum. There was no clear strategy beyond ârewrite things when you touch them.â The 1.3 million line codebase had tangled dependencies everywhere. Custom libraries and Objective-C patterns didnât translate to Swift. The modularization strategy existed in theory but not in practice. And every engineering team needed to ship features, not pause for migrations.
Before I could write any Swift, I needed to understand what actually existed, why the migration had stalled, and how to create a path forward that wouldnât take 8 more years. That archaeology took nearly a year.
The Origin Story
The app wasnât born from a hackathon or a prototype that escaped. It was built by a small team testing a product idea. The product worked. Users loved it. It grew.
Fast forward a few years: the app had become successful, the codebase had grown to over a million lines of code with 750k still in Objective-C, and the companyâs platform standards had evolved. The app? Still using patterns from 2014.
Someone decided: âWe should migrate to Swift.â
They kicked off the effort. Some screens got rewritten. Then⌠it stalled. Teams had features to ship. The migration was important but never urgent. People moved on to other projects. By 2019, it was the thing everyone knew needed to happen but nobody had time for.
The Unique Challenges
Objective-C Patterns That Donât Translate
The codebase was full of patterns that worked great in Objective-C but had no clean Swift equivalent. Custom libraries built for dynamic dispatch. Unsafe patterns that Swift actively prevents. Code that relied on Objective-C runtime magic.
Want to migrate a feature? First, figure out how to translate patterns that Swift wasnât designed to support. Some examples:
- Custom libraries built assuming Objective-C semantics (not RxSwift or Combine compatible)
- Dynamic method resolution and runtime manipulation
- Memory management patterns that donât map to Swiftâs ARC
- Categories with associated objects (Swift extensions canât add stored properties)
- KVO on private properties
You couldnât just rewrite Objective-C to Swift line by line. You needed to rearchitect entire subsystems to use Swift-idiomatic patterns. And do it incrementally while keeping everything working.
Some engineers had solved parts of these problems. But the solutions werenât widely circulated. And even when they were shared, most engineers looked at the approach and thought âthatâs risky, I could break production.â Better to just keep writing Objective-C than risk a P0 incident or spend your weekend fixing a hotfix.
So the migration stalled.
The Modularization That Wasnât
The app was designed with modularization in mind. In theory, features lived in separate modules with clear boundaries. In practice?
Most code lived in Core modules: CoreLibraries, CoreFeatures, CoreIntegration. Features depended directly on Core. Core depended on features. Dependency arrows pointed in every direction. The module graph was a tangled mess.
Want to migrate one feature to Swift? Too bad, it depends on dozens of things in Core. Want to migrate Core? It depends on every feature.
The circular dependencies meant you couldnât migrate anything in isolation. But migrating everything at once was impossible.
This was why the migration had stalled. People would try to migrate a feature, hit the dependency wall, and give up.
Nobody Remembered the âWhyâ
The original architects had moved on. Some left the company. Some switched teams and lost context. The current team maintained the app but didnât necessarily understand the deeper architectural decisions.
Iâd ask: âWhy does this feature reach directly into Core instead of using the API layer?â
The answer: âThatâs just how itâs always been.â
Or: âWhy do we have this custom navigation system instead of using UIKitâs built-in coordinator pattern?â
The answer: âI think someone tried to fix a bug in 2015? Not sure.â
The knowledge wasnât lost because people left. It was lost because the people who knew had moved on to other problems years ago.

The Archaeology Phase
I spent months just understanding what existed and why the migration had stalled.
1. Map the Dependency Graph
I needed to understand what depended on what. The module structure was complex: Core app module, CoreLibraries, CoreFeatures, CoreIntegration. Code that shouldnât be together lived in the same modules.
I used Buckâs query system to analyze dependencies. Built scripts to generate Gephi visualizations showing each moduleâs children and descendants. The graphs were horrifying. Everything was connected to everything.
But they were useful. I could identify hot spots: code that was heavily imported, or code that heavily relied on other things. These hot spots were the bottlenecks. Fix them, and you could unblock entire features for migration.
2. Interview the Veterans
I found everyone who had worked on the codebase. Made a list. Scheduled coffee chats.
âWhy did we build these custom libraries?â âWhat was the original modularization plan?â âDo you remember why this code uses runtime tricks instead of protocols?â
Most people didnât remember specifics. But they remembered problems. âWe needed something fast.â âWe wanted feature teams to own their modules.â âWe tried to avoid massive view controllers.â
The problems were useful. They explained the âwhyâ even when the âwhatâ was forgotten.
3. Find the Stall Points
I looked at every Swift file that existed. What had been migrated? Where did it live? What patterns did it use?
Pattern: Nobody was actually rewriting old Objective-C code. Instead, engineers were writing new features in Swift and using interop layers the mobile platform team had created to bridge between the two languages.
It worked⌠sort of. New code was Swift. But the old Objective-C code stayed Objective-C. And the core features that everything depended on? Still Objective-C.
The migration had stalled because nobody had solved the hard architectural problems. The interop layers let people avoid the problem, not solve it.
4. Understand the Boundaries (Or Lack Thereof)
I traced through code paths. âWhen a user taps this button, what happens?â
The answer was usually: âCode in Feature A calls Core, which triggers something in Feature B, which calls back to Core.â
There were no clean boundaries. No dependency inversion. No protocols defining interfaces. Just direct dependencies everywhere.
To make progress, weâd need to create boundaries that didnât exist.

The Realization
After all this archaeology, the picture was clear.
The migration hadnât stalled because people were lazy or because Swift was hard. It stalled because the architecture made incremental migration impossible.
All the modules were tangled together, so you couldnât migrate one at a time. Many patterns had no Swift equivalents, so you needed to rearchitect entire subsystems. And without understanding why the code was structured the way it was, you couldnât move fast.
The real work wasnât rewriting Objective-C to Swift. It was understanding the existing architecture, creating boundaries where none existed, introducing dependency inversion, building bridges between Objective-C patterns and Swift idioms, proposing API changes that allowed incremental migration, and convincing teams this was worth doing.
Only then could you start migrating code.
The Path Forward
After all this archaeology, I had a plan.
First, fix the dependency graph. Introduce protocols and dependency inversion. Break circular dependencies between Core and features. Create clear module boundaries.
Then, build bridging layers. Create adapters between Objective-C patterns and Swift idioms. Allow Objective-C and Swift code to coexist without forcing teams to rewrite everything at once.
From there, propose API changes. Make features define their interfaces as protocols. Let Core depend on abstractions, not concrete implementations. Create a pattern that teams could follow incrementally.
All of this would enable incremental migration. Teams could migrate their feature in isolation, on their own schedule, while the app stays stable the entire time.
This wasnât sexy work. It was infrastructure. Architecture. Boundary design. But it was the work that would actually unblock dozens of engineers to make progress.
What I Learned
Migrations donât stall because of technical problems. They stall because of architectural ones. If incremental progress is impossible, people will give up.
I could have joined the handful of people rewriting screens. Instead, I spent months understanding why that approach wasnât working. Understanding why something stalled matters more than pushing harder.
If I could go back, Iâd tell myself to budget the archaeology time upfront. Talk to veterans. Map dependencies. Find the stall points. Before you propose a solution, understand the problem.
Sometimes the most valuable work isnât writing code or shipping features. Itâs designing the interfaces that will allow future work to happen. And writing everything down as you go, because that documentation becomes the playbook for the migration.
I loved the archaeology phase, honestly. Thereâs something satisfying about being the person who finally identifies the architectural problems that everyone felt but nobody had named. You become the person who can explain why this is hard, what needs to change, and how to make incremental progress possible.
Thatâs platform engineering. Creating the conditions that let everyone else write code successfully.

Whatâs Next
So here I am, months into a ârevive the Swift migrationâ project. Iâve mapped the dependencies. Iâve interviewed the veterans. Iâve identified why the migration stalled.
Iâve proposed architectural changes: dependency inversion, protocol-based boundaries, bridging layers between Objective-C patterns and modern Swift idioms.
Now comes the easy part: convincing dozens of engineers across many teams that these architectural changes are worth doing. And that we should pause feature work to fix the foundations.1
Footnotes
-
Narrator: This was not the easy part. Stay tuned for Part 2. âŠ
No comments yet. Share on Mastodon and see your comment or write a post on your blog if you support Webmentions
No reposts yet. Share on Mastodon and see your repost or write a post on your blog if you support Webmentions
No likes yet. Share on Mastodon and see your like or write a post on your blog if you support Webmentions
No bookmarks yet. Share on Mastodon and see your bookmark or write a post on your blog if you support Webmentions
Powered by Webmentions