Architecture & Decision Log

From GetX to Clean Architecture: Why We Migrated Holigo One Feature at a Time

October 2, 2026·5 min read·Muchamad Buchori
FlutterClean ArchitectureBLoCGetXMigrationMobile Development

From GetX to Clean Architecture: Why We Migrated Holigo One Feature at a Time

Growing Scale, Reaching Limits: Why the Legacy GetX App Had to Go

Holigo is a multi-service mobile application bringing together diverse transactional solutions into a single ecosystem: utility bill payments, digital top-ups, and travel bookings across buses, trains, flights, and passenger ships.

As the variety of services and transaction volumes surged, it became increasingly apparent that the app's foundational architecture dictates how quickly and safely the team can deliver new features. As the lead mobile developer on this initiative, I was confronted with the reality that our legacy stack was hitting its architectural ceiling.

GetX felt exceptionally convenient and rapid during early prototyping. But for a large-scale app processing continuous financial transactions, that convenience carried a steep price: implicit reactivity often bypassed standard Flutter widget lifecycles, state mutations were notoriously hard to trace, and tightly coupled controllers made automated unit and widget testing fragile and difficult to sustain.

Migration Drivers: GetX vs Clean Architecture
Architecture Evaluation
⚠️ Legacy App · GetX StackScale Bottlenecks
  1. 01Implicit State: Magic reactivity bypasses standard Flutter lifecycle, making state mutations hard to trace.
  2. 02Hard to Test: Business logic and controllers are tightly bound to UI widgets, making unit testing fragile.
  3. 03Inconsistent Code: Lacks strict architectural boundaries; each feature was built with distinct patterns.
  4. 04Tight Coupling: Modifying one service easily causes unforeseen regressions across unrelated flows.
⚠️ High cognitive load & regression-prone as team scales
✦ Target · Clean Architecture + BLoCScalable & Testable
  1. 01Unidirectional Flow: Explicit, predictable Event-to-State transitions powered by standard BLoC streams.
  2. 02100% Testable Domain: Pure Dart domain layer completely decoupled from UI widgets and external libraries.
  3. 03Strict Layer Separation: Presentation, Domain, and Data isolated via formal repository interfaces.
  4. 04Standardized Blueprint: Rapid onboarding & confident refactoring since all features adhere to the exact same blueprint.
✨ Guaranteed transaction stability, high test coverage, multi-team ready
💡

Core Lesson: For apps handling financial transactions, testability and deterministic state flow are non-negotiable foundations, not optional nice-to-haves.

Avoiding the Rewrite Trap: Why We Chose Feature-by-Feature Migration

When a codebase starts showing its age, the instinctive reflex for many engineers is to advocate for a "Big Bang Rewrite" — scraping everything and rebuilding the entire application from scratch.

However, a complete rewrite is a classic software engineering pitfall: the team gets trapped in months of radio silence without shipping tangible value to users, while integration risks accumulate until they simultaneously blow up on launch day. We simply could not afford to disrupt daily operations, user trust, or active business revenue.

Instead, we opted for an incremental migration using the Strangler Fig pattern: the live GetX application remained active in production serving users daily, while new feature modules were built from the ground up using Clean Architecture and phased in one by one. This divided our technical risk into manageable, bite-sized units, allowing each module to be independently tested, verified, and shipped safely to the store.

Execution Strategy: Big Bang vs Feature-by-Feature
Risk Management
💣 Option A: Big Bang RewriteHigh Risk & Delayed
  • ✕Months of Radio Silence: Engineering works in prolonged isolation with zero shippable updates delivered to users.
  • ✕All-or-Nothing Risk: Regressions and integration flaws explode simultaneously on the big-switch launch day.
  • ✕Roadmap Paralysis: Forces a feature freeze on the live app, halting critical business and marketing momentum.
⚠️ Classic software trap that frequently results in missed deadlines
✦ Option B: Feature-by-FeatureRecommended (Strangler Fig)
  • ✓Continuous Delivery: Each completed feature module is independently verified and rolled out to real users immediately.
  • ✓Isolated Blast Radius: If an edge case arises in production, the failure blast radius is strictly confined.
  • ✓Zero Business Interruption: The live application stays fully functional, protecting ongoing revenue and user trust.
✨ De-risked iterations with measurable performance feedback at each milestone
💡

Core Lesson: The most successful architecture migrations are those that never stop the business engine and divide technical risk into bite-sized milestones.

Starting with the Ship (PELNI) Feature: Turning the Smallest Scope into a Blueprint

The choice of the first feature is critical because it establishes the coding standards and architectural benchmarks for everything that follows. Among Holigo's travel ticketing services, we deliberately selected the Ship (PELNI) booking feature as our pilot project.

We chose Ship not because it was the most high-profile feature, but because its business scope was the most compact and isolated compared to flight or train bookings, which involve multi-passenger tiers, baggage handling, and intricate add-on workflows. With a self-contained scope, our engineering team could focus 100% of our energy on crafting a solid, reusable blueprint.

In the Ship feature, we formalized our strict Presentation, Domain, and Data layer boundaries, established deterministic BLoC event-to-state streams, defined pure repository contracts, and standardized network error handling. When flight and train modules followed later, the pattern was already battle-tested, allowing developers to dive straight into feature-specific business logic without re-architecting the basics from scratch.

Migration Sequence: Smallest Scope to Core
Sequential Rollout
01

Ship (PELNI)

Pilot Blueprint

Smallest scope. Ideal laboratory for crafting Presentation, Domain, Data boundaries, BLoC events, and error conventions.

02

Train & Bus

Pattern Expansion

Medium complexity (transit stations, routes & seat picking). Directly leverages patterns established during the Ship pilot.

03

Flight Booking

High Complexity

Multi-tier passengers, luggage add-ons, and dynamic pricing built confidently on an already battle-tested architecture.

04

PPOB & Core

Final Cutover

Migrating daily utility bill payments, auth, and completely deprecating remaining legacy GetX controllers.

💡

Core Lesson: The first feature shouldn't be chosen because it is the most critical or complex, but because it is the ideal candidate to serve as an architecture blueprint.

Foundations and Trade-offs: Keeping Two Systems Living Side by Side

Before migrating a single feature, our shared core infrastructure had to be thoroughly stabilized. We engineered a seamless router bridge for cross-architecture navigation, unified design system tokens, a centralized network client, and a robust dependency injection (DI) service locator.

Throughout the transition period, legacy GetX modules and new Clean Architecture modules coexisted within the same APK and IPA binary. To ensure users experienced zero jarring visual inconsistencies or broken session flows, we enforced rigorous migration guidelines covering state synchronization, standardized color palettes, typography, padding, and confirmation modals across both worlds.

There were undeniable trade-offs: Clean Architecture + BLoC introduces more files, classes, and boilerplate for minor changes compared to the fast shortcuts of GetX. Yet we gladly embraced this trade-off: the crystal-clear separation of concerns and superior testability delivered the long-term confidence and stability our growing team needed.

🏛️Coexistence Foundation: Shared Bridge & Trade-offs
Shared Infrastructure
⚡ Shared Core InfrastructurePrerequisite Setup
🚦 Router Bridge
🎨 Theme Tokens
🌐 Network Client
💉 Service Locator
Legacy Modules (GetX)Still Running

Serves pending features (Flights, Bus, Bill payments) with zero downtime.

Status: Gradually phased out
New Modules (Clean Arch + BLoC)Future Standard

Serves Ship feature with isolated Presentation → Domain → Data boundaries.

Status: Released to production as pilot
💡

Accepted Trade-off: Clean Architecture demands more boilerplate and files for minor changes, but repays the team with testability, unambiguous layer responsibilities, and long-term maintainability.

What You Can Take from This Story

If you're facing a similar architecture migration, here are key takeaways you can apply to your journey:

Pick a first feature that is small but representative: The initial feature functions as the blueprint for everything that follows, so a compact scope allows architectural standards to take shape much faster.

Set the pattern before taking on the second feature: Team-wide consistency is much easier to preserve when layer contracts, BLoC event patterns, and error handling conventions are agreed upon and documented from day one.

Protect visual and session consistency with migration guides: While two architectures live side by side, shared guidelines for state, colors, fonts, and dimensions keep the user experience seamless and cohesive.

Don't wait for a full rewrite before shipping: Incremental rollout divides technical risk into small pieces, catches production issues early, and keeps business momentum uninterrupted.

Closing

Architecture migration is fundamentally not about switching libraries or state managers; it is about reshaping how an engineering team designs, tests, and maintains a digital product over the long run. In Part 2 of this series, we will take a deep technical dive into our Presentation, Domain, and Data layers, using the Ship (PELNI) feature as our concrete implementation example.

Thanks for reading my story :)

P.S. This series is written at the level of engineering decisions and system architecture, without disclosing proprietary code or internal data. If you have questions, feedback, or are planning a similar migration in your mobile team, let's connect and discuss!

Written by Muchamad Buchori · Senior Mobile Engineer
More posts→