When I started this project eighteen months ago, it had a clean architecture. Twelve files. Organized into groups. A model layer. A view layer. A networking layer. It was beautiful and I hated it.
The problem with multiple files is navigation. You're in UserProfileViewController.m and you need to check something in UserService.m. So you Cmd-click, which opens the file. Now you need User.h. Another Cmd-click. Now you can't remember where you were. You press Cmd-Z to go back and accidentally undo your last edit. You've been coding for three minutes and you've already lost context twice.
One night, at 1 AM, fueled by a conviction I can only describe as religious, I selected all twelve files and pasted their contents into a single file called App.m.
That was eighteen months ago. App.m is now 47,000 lines. I have never been more productive.
"The filesystem is a UI for people who can't use Cmd-F."
The Architecture
App.m contains everything. The app delegate. The view controllers. The models. The networking layer. The unit tests (we'll get to this). The constants. A 200-line comment block that used to be the README. Everything.
Navigation is trivial: Cmd-F. Want to find the login logic? Search "LOGIN." Want to find the payment flow? Search "PAYMENT." I've replaced Xcode's entire project navigator with a search bar. It's like going from a filing cabinet to Google. Nobody misses the filing cabinet.
// ============================================ // App.m — The Application // Lines 1–800: App Delegate // Lines 801–3400: Models // Lines 3401–12000: View Controllers // Lines 12001–18500: Networking // Lines 18501–23000: Utilities & Categories // Lines 23001–31000: UI Helpers // Lines 31001–38000: Business Logic // Lines 38001–42000: Tests (yes, in this file) // Lines 42001–47000: Things I'll organize later // ============================================ #import <UIKit/UIKit.h> #import <Foundation/Foundation.h> #import <CoreData/CoreData.h> // ... 847 more imports ...
The table of contents is the most important part of App.m. Without it, you are lost. With it, you are merely disoriented. This is an improvement.
Merge Conflicts
The elephant in the room: if everyone works in one file, merge conflicts must be terrible. They are. Every pull request touches App.m, because App.m is the only file. Every merge is a conflict. We've adapted.
Our workflow: each developer "owns" a line range. I have lines 1–15,000. Melissa has 15,001–30,000. Elliot has 30,001–47,000. If you need to add code outside your range, you file a request in Slack. We call this Range Negotiation. It adds about 30 minutes of overhead per day but eliminates merge conflicts almost entirely.
When we need to add new code and someone's range is full, we have a meeting called The Expansion. We collectively decide whose range grows and whose stays fixed. It's like redistricting but for source code. It's exactly as contentious.
The Testing Situation
Our tests are in lines 38,001–42,000 of App.m. They test the code above them in the same file. The test target's compile sources list has one entry: App.m. It takes 4 minutes and 12 seconds to compile the test target because it has to compile the entire application to run a single assertion.
We've mitigated this by writing fewer tests. Our test count has gone from 340 to 12. But those 12 tests are important. And fast, relatively speaking. 4 minutes and 12 seconds for 12 tests is only 21 seconds per test, which is fine if you don't think about it too hard.
Xcode's syntax highlighting stops working reliably around line 20,000. After line 35,000, autocomplete returns suggestions from the wrong section of the file — I'll be typing in the networking layer and it suggests a UI color constant. After line 40,000, Xcode occasionally beach-balls for 30 seconds. We've reported this to Apple as a performance issue. Apple responded: "This is not how files work."
Results
- Files in project: 1
- Lines of code: 47,312 (as of this morning; it grows about 200 lines/day)
- Average Cmd-F searches per hour: 84
- Compile time (full build): 8 minutes, 47 seconds
- Compile time (incremental): also 8 minutes, 47 seconds (there's one file; every build is a full build)
- New engineer onboarding time: 2 weeks (most of this is memorizing the line range table of contents)
- Engineers who have voluntarily joined the project: 0
- Engineers who have been assigned to the project and stayed: 3 out of 7
The project is a success. The file is a monument. We're considering splitting it into two files when it hits 100,000 lines, but honestly, at that point, what's the difference? In for a penny, in for 100,000 lines.