In this post
One of our apps — a tasks-and-rewards product with a sci-fi theme — is published, works and is used.
Its main.dart is 15,248 lines long.
This post is neither confession nor defence. It is an analysis of how that happens, what it actually cost, and the criterion we adopted afterwards.
How you get to fifteen thousand lines
Nobody decides this. That matters, because the most common explanation — sloppiness — is the least useful one.
The mechanism is economic. At any point in that file's life, the question is the same:
Do I create a new file for these 80 lines, or paste them at the bottom?
And the honest answer, evaluated in isolation, is paste at the bottom. Creating a file costs: an import, a name decision, a folder decision, exporting what needs sharing. Pasting costs nothing.
Every individual decision is right. The sum is wrong. That is technical debt in the literal sense: each instalment is comfortable, and the principal grows on its own.
What it actually cost
Finding things hurt. Text search works; structural navigation does not. You cannot "open the leaderboard file" because there is no leaderboard file. There is a leaderboard region, and locating its boundary is manual work every time.
The real cost shows up when you return after months. The memory of where things were is the index — and it expires.
The editor hurt. Static analysis, autocomplete and formatting are visibly slower in a file that size. Not catastrophic; constant friction — half a second on every operation, all day.
Wide-radius changes hurt. Changing something shared — a colour constant, a currency format — becomes find-and-replace in a file where the same name appears in different contexts. Without module boundaries, there is no natural limit to a change's reach.
Performance did not suffer. File size does not affect the running app. Dart compiles; the binary does not know how many files you wrote. Worth saying, because the intuition on seeing the number is "it must be slow", and it is not.
Shipping did not suffer. The app passed store reviews and updates normally.
The symptom of a monolith is not the app performing worse. It is you working worse. That is why it takes so long to address: it never shows up in product metrics, only in the speed of whoever touches it.
Where the line actually is
The criterion we adopted is not a line count. It is this:
Has the file grown to the point where two people cannot work on it at the same time?
While there is a single author, a large file is mostly an annoyance. The moment a second person joins, it becomes a bottleneck — all work goes through the same file, every commit conflicts, and coordination cost overwhelms any convenience.
A complementary second criterion:
Can you explain where a feature lives without opening the editor?
If not, the structure has stopped communicating. And structure that does not communicate is documentation that rotted.
What we do now
Not a refactoring sprint — and that is the important part of the account. Rewriting a shipped app that works is risk without revenue: the user notices nothing and the chance of regression is real.
The adopted rule is extract on touch:
need to change the leaderboard?
-> extract the leaderboard into its own file
-> confirm nothing changed
-> only then make the requested change
Three good properties: the extraction cost is paid by work that was happening anyway; whatever never changes never gets extracted, and that is fine; and each extraction is small enough to actually verify.
The file shrinks through use. It never reaches zero, and it does not need to.
What we do differently on new projects
One rule, deliberately blunt so it needs no discussion:
A new screen is born in its own file, even at thirty lines.
The cost is one import. The benefit is never having the "where do I put this" conversation, because the answer is already decided.
And a second rule that prevents half the monoliths we have seen: state and presentation in separate files from day one. Merging the two is what makes a file grow in both directions at once.
What this says about a small studio
In a large organisation this file would have been blocked in review, and rightly so — the coordination cost would be immediate.
In a one- or two-person operation the maths differ. A shipped app producing results is worth more than the correct architecture of an app that never launched. This monolith is not the best technical decision — it is the decision that let the app exist.
The mistake was not writing it that way at the start. It was not defining, at the start, the trigger that would force the change.
Frequently asked questions
Do you recommend doing this?
Not as a default. We recommend understanding why it happens, because the mechanism produces monoliths in any codebase: each addition is individually cheap, and nobody ever decides to have fifteen thousand lines. The decision is never made; it accumulates.
Why not refactor it all at once?
Because a large refactor of a shipped app is risk without revenue. Our approach is extract-on-touch: whenever a part needs changing, it gets extracted before being changed. The file shrinks through use, not through a rewrite sprint.
Didn't static analysis warn you earlier?
It did, and the warning was ignored because there was no pain attached. A complexity metric with no associated symptom is easy to dismiss — the symptom only appears when someone new tries to work on it, or when you come back after three months.