Back to blog

Why Refactoring Matters: Lessons from Refactoring a Production Mobile App

The codebase that made me a better developer — a story of technical debt, architecture decisions, and the real cost of 'we'll fix it later'.

Six months ago, I joined Tripex as a Flutter developer. On my first day, I pulled the mobile app repository and ran flutter build. Then I waited. And waited. When the build finally finished 8 minutes later, I knew I had my work cut out for me.

What I discovered was a codebase that had grown organically over two years—features layered on top of features, business logic scattered across UI files, and state management that seemed to follow no discernible pattern. It worked. It was in production. But it was also a minefield.

This is the story of why we refactored it, what we learned, and why good architecture isn't just nice to have—it's essential for survival.

The Real Cost of Technical Debt

Technical debt doesn't announce itself. It accumulates in small decisions:

  • "I'll just put this logic here for now"
  • "We don't have time to set up proper state management"
  • "This API call can live in the widget, it's just one screen"

At Tripex, these decisions had compounded. The app had:

  • Inconsistent state management - Some screens used setState, others used Provider, a few used Riverpod, and one mysterious corner used BLoC
  • Widget files exceeding 1,000 lines - Business logic, UI, and API calls all living together
  • No separation of concerns - The concept of "layers" was more suggestion than architecture
  • Testing? What testing? - Zero unit tests, minimal widget tests

The app worked. Users were happy. But every new feature took longer than the last. Every bug fix risked breaking three other things. Onboarding new developers took weeks because understanding the codebase required holding the entire mental model in your head at once.

The Breaking Point

Our breaking point came when we needed to add a major feature: multi-language support. What should have been a straightforward implementation—swap out some strings, add a locale switcher—became a nightmare.

Text was hardcoded everywhere. Some strings lived in widgets. Others came from API responses with no localization keys. Some were concatenated dynamically. After two weeks of hunting down every hardcoded string, we realized the real problem: we weren't just adding a feature. We were fighting our own codebase.

That's when we made the decision: we were going to refactor. Not a rewrite—that's usually a trap—but a systematic refactoring to clean house.

The Refactoring Strategy

We didn't shut down development for three months. Instead, we adopted the Boy Scout Rule: leave the codebase better than you found it.

Week 1-2: Establishing the Foundation

We started with architecture. We chose Riverpod for state management—not because it's trendy, but because it provided the consistency we desperately needed. We established clear layers:

  • Presentation layer: Widgets, dumb and focused only on UI
  • Business logic layer: Services and controllers handling business rules
  • Data layer: Repositories managing API calls and local storage

Week 3-6: The Great Extraction

Screen by screen, we extracted business logic from widgets. A 1,200-line widget became:

  • A 150-line widget focused purely on UI
  • A controller class managing state and business logic
  • Repository methods handling data operations

The immediate benefit? We could write unit tests for the first time. When business logic lives in widgets, it's nearly impossible to test. When it's in a controller, testing becomes straightforward.

Week 7-10: Feature Flags and Gradual Migration

We used feature flags to deploy changes gradually. New architecture for new features, refactored code for old ones. This let us validate our approach without risking everything.

Week 11-12: Cleanup and Documentation

We deleted dead code. So much dead code. Features that were built but never launched. Experiments that became permanent by accident. Thousands of lines, gone.

We also documented our architecture decisions. Not just for future developers, but for ourselves—so we wouldn't make the same mistakes again.

The Results

Six weeks of focused refactoring yielded results we couldn't have imagined:

Build time: 8 minutes → 2 minutes Less code, better structure, fewer dependencies to resolve.

New feature implementation: 2 weeks → 3 days Clear separation of concerns meant we could work on UI, business logic, and data layers in parallel.

Bug rate: 40% reduction Fewer side effects, better test coverage, and code that was actually readable meant fewer bugs shipped to production.

Developer onboarding: 3 weeks → 3 days New team members could understand the architecture in hours, not days. The codebase had a clear rhythm.

Test coverage: 0% → 68% For the first time, we had confidence in our deployments. Refactoring with tests felt safe. Without them, it would have been terrifying.

What I Wish I Knew Earlier

Architecture Isn't Premature Optimization

There's a myth that "we'll fix it later" is a valid strategy. It's not. Technical debt compounds like credit card interest. The longer you wait, the harder it is to pay down.

Good architecture from day one doesn't mean over-engineering. It means thinking about separation of concerns, about where logic should live, about how your app will grow.

Refactoring Isn't a Dirty Word

Developers often treat refactoring as admitting failure. "If I had written it right the first time..."

Here's the truth: you can't write perfect code when you don't fully understand the problem. The first implementation teaches you what the system actually needs. Refactoring is the process of applying those lessons.

The Best Time to Refactor Is Continuously

Don't wait for a "refactoring sprint." Make it part of your daily work:

  • See duplicated code? Extract it.
  • Touching a messy widget? Clean up what you're working on.
  • Writing a new feature? Don't repeat the mistakes of the past.

Small, continuous improvements prevent the need for massive refactors.

Tests Are Your Safety Net

I can't emphasize this enough: don't refactor without tests. Our first attempts at refactoring broke things because we had no way to verify behavior hadn't changed.

Once we had test coverage, refactoring became... dare I say... enjoyable? We could make bold changes with confidence. The tests told us when we broke something immediately, not after deployment.

Practical Takeaways for Your Next Project

If you're starting fresh or inheriting a mess, here's what I'd recommend:

  1. Choose your architecture deliberately - Don't just use what you used last time. Consider your team's size, the app's complexity, and your growth trajectory.

  2. Separate concerns from day one - UI handles UI. Business logic handles rules. Data layer handles data. Don't mix them.

  3. Write tests as you go - Retrofitting tests is painful. Writing them as you build is natural, and it makes refactoring later possible.

  4. Document your decisions - Why did you choose this state management solution? Why this folder structure? Future you (and your teammates) will thank you.

  5. Refactor continuously - Leave every file better than you found it. Small improvements compound.

The Bottom Line

Refactoring the Tripex app was one of the hardest things I've done as a developer. It required discipline, patience, and the courage to change what was already "working."

But it was also one of the most valuable experiences. I learned that good architecture isn't about following patterns—it's about making your codebase a place where developers can be productive, where features can be added without fear, and where technical debt doesn't accumulate like sediment.

The app that took 8 minutes to build and terrified us to touch now builds in 2 minutes and welcomes new features. The team that dreaded certain parts of the codebase now understands it confidently.

That's the power of good architecture. That's why refactoring matters.