Why a mobile rewrite is rarely the right answer
Aging mobile applications tend to accumulate the usual problems: oversized view controllers, direct UI manipulation, poor separation of concerns, and limited testability. Those issues can be managed with discipline early on, but once an app has grown chaotically inline with the business it supports, incremental change becomes genuinely difficult. The traditional escape hatch has been to build a new app and release it all at once.
That approach is getting harder to justify. Mobile apps are no longer simple single-purpose tools; they have evolved into multi-function platforms that handle payments, social features, content, and commerce. Rebuilding that kind of application in one shot means accepting enormous risk, long delivery timelines, and the possibility of damaging app store ratings and user trust.
The Strangler Fig pattern offers a way out. Instead of replacing a legacy system in one big-bang release, a new system is built around it incrementally and allowed to gradually take over. The pattern is well established in server-side development but rarely applied to mobile applications. One reason is that several prerequisites need to be in place first. The broader framework for legacy displacement breaks down into four stages: understand the desired outcomes, decide how to break the problem into smaller parts, deliver those parts successfully, and change the organization so this can continue on an ongoing basis. Only at the third point does the Strangler Fig pattern come into play. Attempting it without the preceding groundwork is setting up for failure.
Building the architecture to support strangulation
A successful strangulation depends on having a modular architecture in place from the start. The mobile app should be decomposed into modules that map directly to bounded contexts or domains. This allows old code to be replaced incrementally at the domain level rather than by attempting to swap out the entire application at once.
Communication between modules must be organized. A prominent enterprise client of Thoughtworks adopted this architecture with clean map navigation: a navigation service coordinated by the shell or host app, and a contract-based deeplinking approach. When a domain requires data from another module, that module exposes a service that other modules can consume, implemented either with an interface in the same module or a service locator pattern. This keeps all domain-level communication explicit.
Implanting the Strangler Fig
With the modular architecture in place, the actual strangulation begins by identifying which modules of the legacy app should be modernized first. Domain prioritization is guided by business value and risk analysis. In the trial at this enterprise, the focus was placed on a domain where the team wanted to increase delivery cadence and improve customer outcomes, without having to rebuild the entire application.
The key structural component is a journey host app. This host acts as a router that maps which implementation of each domain should be displayed: the legacy one or the new one. When a user navigates to a domain that has been modernized, the host loads the new code; otherwise it falls back to the legacy implementation. This mapping can be controlled dynamically, allowing gradual rollout to a subset of users.
BitBucket Pipelines were used as the CI pipeline. Everything needed to be containerized, with build and test stages running in Docker containers to maintain consistency. A single command could rename the app ID and package name to map to the appropriate build variant, so the pipeline could create a light app, the real shell (loadable or not), and later the experiments. Build times were something that needed active management, requiring carefully staged pipeline steps and shared caches.
Communication through the native bridge
In the strangler fig approach, the native bridge between the host app and module code plays a central role. The shell app presents a native wrapper around the web content rendered by each module's JavaScript code. There is no single SDK to add; instead, each module of the loadable app is a whole web application in itself, built from a plugin-based architecture. The React app exposes extension points that enable plugins contributed by the shell owner to override certain parts of the module's UI with their own.
Bi-directional communication between the module and the shell flows through methods such as window.setSharedData() and event listeners. The shell holds the source of truth on relevant platform data, and the module subscribes to those events with window.addEventListener() to react to changes in that state.
Error reporting flows in the opposite direction. When an error occurs in the module code, it invokes an API on the shell such as Diagnostics.error() to log errors uniformly through the native application. The bridge extends to performance monitoring and logging in an associated API portal, forwarding all logs to the relevant platform.
Navigation is merely a change in state from the deep linking registry. The bridge exposes an interface to notify the shell that navigation should occur. This API enables a flexible routing mechanism where a module can pass in the uri and origin of navigation requests through native bridge invocation.
Handling sensitive data and testing boundaries
Compared to authorizing payments, handling sensitive data in local storage is a lighter cross-domain concern, but it can be handled by providing a third artifact containing the authentication web page itself. A shared session helper manages tokens and user info on behalf of the shell and modules. When the module makes an HTTP request, the request contract determines whether the token should be sent automatically. When APIs reject requests, the module can use the helper to signal that an action should prompt the user or force reauthentication.
The team chose not to go down the automatic regression testing route. Instead, manual regression tests were organized around the bounded contexts that were being affected by change, integrated into the test plan of the sprint. A regression pack covering all major user journeys was executed in key domain areas before the experiment began. The theory was that since those domains were effectively isolated by the modular architecture, testing at those boundaries provides the necessary protection without ballooning into whole-app regression testing.
Running the experiment
To truly understand the metrics associated with this modernization approach, the client launched an experiment with a subset of users. The app was set up so that the legacy app would remain in the store alongside the experiment. The experiment itself, the shell app with the legacy domain modules internally, was released to a cohort of app store users. The iOS and Android apps were opened to the new app bundle, and users were disabled from making purchases should they get to session timeouts or errors.
Guard rails were included to keep the experiment safe: if the shell app detected that something was wrong, alternative navigation data could disable new modules, or the data layer could signal a kill switch. Users to be included in the experiment were filtered based on the user ID of the app store account, and the event analytics framework tracked the hypothesis event, delivery metrics and session replays on new domain screens.
Incremental replacement was bounded to the specific target domain that had been identified and agreed prior to kick-off. A two-week sprint was planned to build the loadable module artifacts before integrating them with the shell. Overall the team delivered the original hypotheses tagged from the three-month plan within a target of ten weeks.
The pay-off: faster value, faster delivery
The trial produced measurable improvements in delivery metrics. The table below compares the two most significant metrics between the legacy app teams and the adoption teams across two consecutive release trains.
Cycle time, the lead time from start to finish of a release cycle, showed a 50 percent reduction in the median. The number of releases that were put into production increased, and a significant portion of functional requirements were delivered faster than they had been previously, meeting the aggressive timeline for key differentiators. Capacity increased by 30 percent as tracked by velocity, despite the team having a late change in product owner and three new team members who were still ramping up and required assistance. The in-process lead time was reduced from ten days to three, while the downstream lead time was also reduced by up to fifty percent.
Trade-offs and limitations
This approach comes with added build and architecture complexity: a build pipeline that must cater to matrixed development both on the API server and the core app, an architecture that must accommodate the strangler's routing rules at the domain level to keep the code adequately decoupled, and regression testing that requires effort relative to the modular architectural boundaries. There is risk that an engineer might abuse the plugin or dependency injection architecture by injecting invalid code into an otherwise working module, so attention to test strategy and development practices must remain high.
At a bit level there were trade-offs from packaging two applications into the same shell. Apart from being a lot to retrofit and learn, it adds another security footprint and provides an extra layer for man-in-the-middle opponents to attach to. It makes dynamic builds more complicated. None of this is insurmountable, but it does take time, so teams need a reasonable stabilization period to arrive at a final state in terms of code freeze, iteration, bug fixes, and legacy removal.
Every framework will inevitably have bugs and constraints. The capabilities in this implementation were targeted at the use case described here, and there may be edge cases beyond the bounds of what was built. The code samples and companion approaches described are illustrative only, and building entirely with them without engaging security and hosting reviews is not advisable.
Measurements
A quick note on measurements: while a lower median cycle time was reported, the mean was generally larger because of outlier days, typically caused by pipeline and infrastructure concerns. The adoption teams peaked with a mean of 12.3 days to release, falling to 8.8 days, and the earlier weeks showing 18.7 days due to a payload bug that required rework. While the ten-week period covered two release trains rather than full quarterly planning, enough data was collected to demonstrate the likely character and scale of benefits.
What this means for mobile legacy modernization
Adopting the Strangler Fig pattern in a mobile context offers a path to modernization that maintains the existing user experience while implementing a fully deployed product incrementally. The approach allowed the enterprise client in this case study to reduce build complexity within their legacy mobile estate by focusing modernization on their largest known challenges, speeding up release train cadence, and shrinking cycle time. The ability to scale the architecture had a positive effect on developer experience, delivery speed, and process efficiencies. With the pattern in place, future platform changes such as feature-unified authentication or multi-domain features could be accomplished more cheaply and without exposing the business to meaningless re-platforming risk.
Why a Full Rewrite Was Off the Table
The client that motivated this work was a global retailer with a mature mobile estate. Its smaller sub-brands had already been modernized using a Modular Micro-app architecture built on React Native, allowing teams to package a domain’s bounded context as an importable component backed by its own backend for frontend (BFF). That approach cut duplication, reduced cognitive load, and let the organization ship shared features like auth and grocery shopping across brands quickly.
The home-territory app, however, was a different beast. It served the main brand, generated far more revenue, and had accumulated years of features. Growth had slowed change frequency from days to months. The backlog was large, stakeholders were frustrated, and the release cycle had become risk-averse: any outage meant serious revenue loss for an essential product. Every change was exhaustively tested before reaching users.
The organization first evaluated a complete rewrite and was shocked by the cost and duration. A big-bang release to app store customers felt unacceptable given the user volumes involved, even with alpha and beta test groups. Modernizing along the same lines as the sub-brands was judged to carry similarly high cost and risk.
Thoughtworks proposed a different path: combine the modular architecture already proven in the sub-brands with the Strangler Fig pattern. Instead of replacing the whole app at once, the team would incrementally replace individual domains, reusing production-ready micro-apps inside the legacy experience. This delivered value sooner, with less duplication, and let the business measure how well the new product was received before committing to full replacement. The goal was confidence in the pattern and the product, not a polished cohesive shell from day one.
Interception and Incremental Replacement
The proof of concept began by identifying domains and their navigation routes—what the team called points of interception. Navigation in mobile apps is usually well encapsulated, so the team could be confident about steering users to a chosen experience. From there, they selected a domain for incremental retirement, using the Grocery domain as the example.
The new Grocery domain was a micro-app already running in the sub-brand apps. The key move was embedding an entire React Native application inside the existing legacy native app. Following good modularity practices, Grocery was built as an encapsulated component, meaning each new domain added to the strangler application could be enabled or disabled individually.
In the legacy app, Grocery had been underpinned by a monolithic backend. The imported micro-app was configured to use that same backend at first. Each micro-app carried its own BFF, which in this case served as an anti-corruption layer. The BFF isolated the frontend from the monolith’s model, talking to the existing system through the same interfaces the legacy mobile app used, and translating between the two models in both directions as needed. That kept the new frontend from being constrained by the legacy API while it evolved.
The process was designed to repeat on the next prioritized domain, working inside-out until the native app became a shell containing the new React Native application. At that point, the old native app could be removed entirely. The replacement would already have been tested by the existing customer base, the business would have confidence in its resilience under real load, developers would find features easier to build, and the big-bang risks would have been avoided altogether.
From Monolith to Micro-App: Making the Switch
Putting the Mobile Strangler Fig pattern into practice requires answering several implementation-level questions. The first is structural: how do you build and maintain both native and non-native code without duplicating effort?
Inverting the Repository
The original native application structure was turned inside out. Instead of nesting a React Native (RN) directory inside each mobile operating system’s folder, control was inverted so the React Native app became the top-level container. The default react-native init template provided a structure that could embed the iOS and Android subfolders directly, avoiding significant duplication.
From a developer’s perspective, little changed. The two operating-system-separated teams kept targeting their original directories, just now within a single repository. That generalized pipeline, applicable to both iOS and Android, shows how the client’s code flowed from the app’s entry point.
The Native Bridge and Micro-App Communication
Navigation between legacy and new code happens at the “points of interception” mentioned earlier. The underlying mechanism is the React Native Bridge: a message queue for instructions such as rendering views, calling native functions, handling events, and passing values. Properties like isCartOpen and sessionDuration travel across it, while bridge function calls can invoke native modules — for example, JavaScript calling the device’s geolocation module.
A “React Native Micro App” is a self-contained bundle of UI and functionality for a single domain, similar to the micro frontend pattern. This encapsulation is what allows the Strangler Fig to grow with control. If one journey earns more confidence than another, traffic can be routed disproportionately to the trusted micro-app without impacting the others.
The bridge carries state and actions between experiences, preserving local UI state when a user crosses from one domain to another. This allowed the team to decouple domains at natural fracture points without losing local state.
Secure Handling of Sensitive Data
Local state sharing is one thing; authentication credentials are another. The client had recently rebuilt their login and registration experience as a modular, configurable, brand-agnostic React Native module for other customer-facing apps. Reusing that module in the Strangler Fig served as the pattern’s first real demonstration.
When a customer logged in or registered within the new React Native journey and then moved back into a legacy screen, their authentication status had to persist. The solution used the native module side of the bridge: a React Native library wrapped the platform keychains — Android’s EncryptedSharedPreferences and the iOS Keychain — saving authentication data after a successful login. Because those stores handle flexible data structures, the same (re)authentication logic worked regardless of whether the user was in the native or non-native portion of the app. The approach also established a pattern for securely sharing any sensitive data between experiences.
Testing Across Domain Boundaries
An embedded Strangler Fig creates a testing challenge: multiple teams, numerous branching paths, and journeys that jump in and out of different codebases. The team needed to know, from any vantage point, when a change broke overall functionality.
The client’s codebase already followed the test pyramid, with unit, subcutaneous, and journey-centric UI-driving tests. The strategy was to keep that pattern: expand across all layers and extend journey tests to cover crossing the embedded app boundary. The ownership problem, though, was real. Tying one team’s build success to code they did not write was unreasonable. The proposal was a test ownership strategy across teams:
| Test Type | Native | React Native |
|---|---|---|
| Unit | X | X |
| Subcutaneous | X | X |
| Legacy Journey | X | |
| e2e Micro-app Journey | X | |
| Contract tests for interactions with ‘The Bridge’ (journeys with both legacy and micro-app components) | X | X |
For Native-to-RN interactions, these contracts act as blueprints for micro-apps and enable unit testing with mocks. Mocks simulate the micro-app’s behavior and verify it uses required context correctly. For RN-to-Native calls, React Native exposes the NativeModules object; mocking it allows assertions against the resulting context.
Defining those responsibility boundaries limited the cognitive load of regression-related testing on each team by introducing “hand-off” points without sacrificing overall coverage.
The strategy was well received by both native and non-native teams, but the contract tests themselves proved complex to implement. The legacy application team lacked the bandwidth to learn and write a new test category. As a compromise, the React Native team wrote all contract tests for the duration of the proof of concept. The lesson: any interstitial state demands attention to the developer experience, and layering complexity onto a team is only part of the solution.
The Experiment
To demonstrate success, the team needed measurable outcomes from two different experiences — and the ability to revert quickly if something failed. The organization already had an experimentation tool integrated, so it was used for both metric capture and experiment measurement. Device-level user selection (via IMEI) was chosen over account-level selection because a single account could be used across multiple devices, skewing results.
The tool’s feature-flagging component allowed the experiment to be switched off — reverting to the native app only — without a new release. Recovery time in an outage scenario was thereby greatly reduced.
Measured Results
The client selected a domain/journey that mapped to an existing smaller micro-app as the first incremental replacement. That micro-app was already proven in other applications across the business and generic enough to be easily white-labeled. A second, larger micro-app was then integrated to prove the pattern’s extensibility.
Time to First Value
For this metric, the clock starts when the Strangler Fig framework is created inside the legacy app and includes all regression and integration activities around the first micro-app. The client had been quoted roughly two years for a complete rewrite. With the Strangler Fig, it took about one month to implant the micro-app structure, three months to build the first micro-app, and five months for the second. From a blank page then, four months from start to first value. The client actually saw first value much sooner, because both micro-apps had already been built for use in other mobile applications. The time to first value in practice was just the one-month implantation period.
Cycle Time
Cycle Time here measures the time to make a change inside a micro-app’s code, including regression testing against the Strangler Fig app. It excludes app-store push, a variable-length process independent of app type. For the legacy app, cycle time was the duration to make and regression-test a change in the native codebase.
The metric matters because an uplift signals reduced organizational risk aversion. Changes that once demanded exhaustive testing — due to potential for unrelated side effects and outages — become cheaper when a domain is fully encapsulated. A team owning a micro-app can test nearly all changes inside that micro-app, mapping any bridge invocations to boundary contract tests.
| App Type | Median Cycle Time (over 30 days) |
|---|---|
| Micro-App 1 | 9 days |
| Micro-App 2 | 10 days |
| Legacy App | 20 days |
The comparison shows a significant uplift in speed for changes made inside encapsulated domain boundaries when set against the coupled monolithic app structure.
What the Pattern Doesn’t Solve Yet
The Strangler Fig approach has clear benefits, but it is not a universally proven replacement strategy. Our implementation began as a proof of concept, driven by a client’s refusal to accept a full rewrite as the only path forward. While our metrics for delivery speed and cycle time are promising, we lack data from the right side of the development process. Before the pattern can be recommended as a general solution, we need evidence on operational resilience, including time to restore service and outage frequency and severity.
There is also an open question about scale. We applied the pattern to only two of the client’s many application domains. It is unclear whether adding more domains to the interstitial app state will introduce unforeseen complexity or degrade the benefits we observed.
What We Learned
Our proof of concept demonstrated that the Strangler Fig pattern can substantially shorten the time to first value compared with a full rewrite. Modular micro-apps delivered a 50% improvement in median cycle time over the legacy application. These results are encouraging, but they come with caveats: we incurred accidental complexity that required active management, and our status as a PoC means we cannot yet vouch for the pattern’s long-term resilience or scalability.
The Bigger Picture
Mobile apps will keep growing in scope and complexity, and teams will increasingly prioritize risk mitigation and faster value delivery when modernizing. That shift may call for an approach like the one described here. But the Strangler Fig is not a silver bullet—it is one tool in a broader legacy modernization toolkit.
Anyone considering this pattern should recognize that legacy modernization is a multifaceted problem requiring serious analysis and alignment before any code is written. The upfront investment pays off twice: it helps you select the right approach for your situation, and it ensures the resulting application is better aligned with the customers it serves and the problems it solves.



