Mobile app refactoring: where debt accumulates
Technical debt in a mobile app rarely looks like a single broken module.
Usually it accumulates quietly: the network layer has grown exceptions for specific screens, business rules have spread across the screen, cache, and analytics, and the expanded codebase no longer fits in even a senior engineer's head. One person makes a change, and the whole team deals with the consequences.
Module coupling grows along with the number of scenarios. To show a new card in the catalog, a change has to be made to the data model, the loading layer, the list markup, and local storage — four places, three of which have nothing to do with the task.
The tighter the coupling, the less predictable it is what exactly will break on a neighboring screen.
Then regression after a change kicks in. A change in the response format drags along checks of checkout, authorization, and notifications, because they all read the same data.
A manual run takes days, automated tests do not cover all branches, and the release is pushed back indefinitely.
For the product, this means small improvements stop being small. The team plans not what the business needs, but what is safe to change; instead of development, there are batches of fixes and unscheduled releases. Developers start to be afraid to touch old areas — and debt grows even faster.
At the same time, what has accumulated is measurable: by the number of connections between modules, build time, the share of paths without tests, and the number of defects per release. These figures show exactly where to cut. Below we break down how this is addressed.
The effect of refactoring: faster releases and fewer failures
Refactoring is justified when its consequences are visible in release cycle metrics, not in code neatness.
As long as screens, the network layer, and storage are directly connected, a change in one place drags a chain of changes: the build takes longer, verification is manual, and part of the time is eaten by regressions that surface after publication.
Splitting into independent modules changes the picture. Areas are built and tested in parallel, a typical change is localized within one module, and automated tests for critical scenarios run on every code change.
Faster releases here are not a slogan: the share of time the team spends preparing a release is cut several times over, and the release itself becomes a planned operation rather than a crunch.
We calculate the reduction in the number of failures based on facts: the share of crash-free sessions in Crashlytics, the number of returns after publication, the frequency of rollbacks to the previous build.
Clear module boundaries remove an entire class of crashes — those that were born at layer junctions and appeared only under load.
We also run peak scenarios separately: load resilience grows because heavy operations move off the main thread, and repeated network requests are not duplicated.
The fourth effect is the labor intensity of app maintenance and predictability of estimates.
When modules are each responsible for their own area, a new task is estimated by the amount of work within one area, not “as luck would have it”: the spread of estimates narrows from weeks to days, and the release cycle is planned by calendar, not by fact.
Approaches to rework: full, gradual, targeted
The rework mode is chosen based on two questions: how deeply the problem sits in the architecture and whether releases can be frozen during the work.
None of the options requires stopping the current product — the difference is how much transitional code the team carries in parallel and how long two layers live at the same time.
| Option | When it fits | Limitations |
|---|---|---|
| Targeted optimization of bottlenecks | One or two scenarios degrade: cold start, scrolling a long list, background synchronization. The rest of the core is fine | Treats the symptom. Debt in the data access and navigation layers remains, and after several iterations changes return to the same modules |
| Gradual rework | The product is actively evolving, releases go out every two weeks, and stopping is impossible | The old and new layers live in parallel: the amount of transitional code grows, and compatibility checks are needed at every step |
| Extracting modules into a separate library | Shared logic is repeated in two or more apps or teams | Requires a version contract and its own release cycle, otherwise builds diverge and users get different behavior |
| Full rewrite of the core | The architecture prevents the product from evolving, platform interfaces are outdated, and changes cost more than a new implementation | Parallel support of the old branch until the new one reaches feature parity, and a full testing cycle again |
They usually start with targeted optimization: it quickly shows where the real bottleneck is.
If changes after it return to the same modules, they move to gradual rework and extracting shared parts into a separate library, while a full rewrite of the core is left for the case when the architecture can no longer support the load of development.
How the work proceeds: stages from audit to handover
Refactoring breaks the product when it proceeds as one big rewrite: while the branch lives for weeks, releases stall, and regressions surface among users. We take short steps; after each one, the app remains buildable and working. The order below is typical — the scope and depth change for a specific project.
- Codebase audit. We examine the change history and dependency map, find duplication and the modules that change most often. The output is a list of bottlenecks with risk assessment and work order.
- Target structure design. We break the app into layers and modules and fix the contracts between them. What we migrate as is and what we rewrite — we agree with your team before the first line of new code.
- Pilot module. We migrate one area entirely to test the design on live code and estimate the labor intensity of the rest. Then we scale the proven approach, not assumptions.
- Incremental migration. We proceed in small portions: each change is covered by a test and goes into the main branch without waiting for the work to finish. Releases continue on the usual schedule.
- Automated build and checks. We set up a build for every change, static analysis, and test runs. Regressions are caught before code review, not on rollout day.
- Acceptance checks. We compare behavior with the original version across key scenarios — login, payment, offline mode. Start time and crash rate must not worsen at any step.
- Handover and team training. We provide structure documentation, rules for making changes, and a walkthrough with the customer's engineers. The app remains maintainable without us.
Project artifacts: diagrams, regulations, documentation
Refactoring is complete not when the build turns green, but when the repository contains artifacts that let the team work without us. Each one sits next to the source code and is updated in the same change, so it does not diverge from the code a month later.
- Module and dependency map — which modules exist, who depends on whom, where cyclic dependencies are broken, and which layer is responsible for what.
- Architecture documentation — a diagram of layers and domain boundaries, extension points, a decision log with rationale and rejected alternatives.
- Code workflow regulations — branching model, mutual review procedure for changes, size limits, mandatory checks before merge.
- Test description — which automated tests cover which paths, where the coverage boundary lies, and what remains for manual verification.
- Release instructions — build procedure, signature preparation, rollout to the channel, and rollback to the previous release.
- Ownership map — which module has an assigned owner and whom to go to with a question.
- Technical debt log — what is consciously deferred, by what signs we will return, and what will happen if we do not.
We check the set simply: an engineer who did not participate in the refactoring builds a release according to the instructions. If they manage without hints, the artifacts are complete; if they get stuck, we finish writing them before handover.
Case study: release time cut by a factor of three
What it was like
The app had been evolving for several years without a shared architecture: screen logic, network operations, and state storage were mixed together, and the same calculation was duplicated in four places.
The Flutter codebase was built with a single command, but every change dragged along a chain of related edits.
A release took six weeks, three of which went to stabilization after regressions. About 40% of tasks were returned for rework, 60% of changes required edits in related modules, and a new engineer became able to handle independent tasks only after a month.
What was changed and what the result was
We split the app into layers with clear boundaries: presentation, business logic, data access.
Shared calculations were consolidated into one module, the network layer was closed behind a single client with error handling, and critical paths — payment, authorization, synchronization — were covered with automated checks.
The release was moved to a pipeline where build, checks, and signing run as one command without manual steps.
The release time was cut by a factor of three — from six weeks to two. The share of tasks returned for rework fell from 40% to 8%, and the share of changes with edits in related modules — from 60% to 15%.
Onboarding a new engineer to the project takes about five business days instead of three to four weeks: the structure is predictable, and checks show an error before manual code review.
How does a rewrite from scratch differ from gradual rework?
A rewrite from scratch is a second project alongside the first: logic is migrated into a clean architecture, and the old code is frozen. Gradual module displacement works differently — a new layer appears inside the working app and takes over area by area.
Expensive in a full replacement is not the amount of work, but the pause in delivery. While the migration is underway, neither new features nor fixes are released, requirements move ahead, and launch shifts by the amount of that lag.
The risk of a full replacement is that the failure here is single and shared: the platform does not ship partially — it does not ship at all.
A full replacement is justified when the problem is in the foundation: connections are spread across screens, there is nowhere to connect a new module, tests are not written, and a change drags half the app with it. Then displacement costs more — each layer accumulates caveats, and the scaffolding eats the gain.
Gradual rework almost always delivers results faster. The first replaced module reaches users in one or two cycles and is tested in live behavior; then the share of new code grows, and rollback of a step is limited to one area. Product releases do not stop, and rework timelines are calculated by stages, not by a single date.
We make the choice based on measurement: how much code actually needs to be replaced and how many external dependencies rest on that area. If less than a third changes and the connections can be untangled — we go through gradual module displacement.
If the core gives no foothold — we plan a full replacement and decide in advance how the current branch lives until the switch.
Common technical doubts before starting work
Refactoring a mobile app almost always happens on a live product: releases cannot be stopped completely, otherwise users do not receive fixes and store positions drop.
That is why the rework is broken into short changes — each one passes the build and goes into the nearest release separately, while the main branch remains ready for deployment at any moment.
Part of the code does indeed remain partially rewritten, and this is a working state, not incompleteness: module boundaries are fixed, the old layer is called through stable interfaces and is removed as the replacement becomes ready.
Will releases have to be paused during the work?
No. The work proceeds in parallel with the release cycle: changes are merged one at a time, and each passes checks before merging. If a change does not pass, it remains in a separate branch and does not delay the other tasks.
How do you make sure nothing is broken?
Regression checking is based on automated tests for critical paths — launch, authorization, payment, transitions between screens — and runs on every change in Appium.
Additionally, we compare crash reports of the new build with the previous release: a noticeable increase in errors blocks the merge until the causes are analyzed.
How accurately do you estimate timelines?
Estimation is done by modules, not for the code as a whole: first an inventory of dependencies and test coverage, then the work order and points where work can stop without losing functionality.
This way timelines are recalculated as each module is completed, and you see the scope at each stage, not a single overall figure.
Let's discuss the task and come back with a work plan
Estimating refactoring without access to the code turns into a “from and to” range that diverges from reality in the very first week.
To name timelines by stage and the work order, we need inputs: how the repository is structured, how releases go out, and where exactly it hurts now. Send them in any convenient form — text, link, or voice — and we will review.
- Repository access: read permissions are enough — the module structure, change history, and the branch from which the release is built are important.
- Release scheme: how often builds go out, who builds them, which testing channels are used, and where a manual step remains.
- Description of bottlenecks: long builds, crashes during app operation, duplicated logic between screens, places that are edited with caution.
- Map of modules and screens with notes on which are under active development and which are frozen.
- What must not be broken: contracts with the backend, the on-device data storage scheme, the design system, authorization behavior.
- State of automated tests: share of covered paths, what fails in the suite, and what is checked only manually.
- Product priorities for the next cycle — tasks because of which refactoring cannot be paused for long.
With this set, we come back with a plan: the sequence of stages, what each includes, what we consciously leave out of scope, timelines, and scope approval points. Describe the task — we will come back with an estimate and plan, then we fix the work order and start.







