Wednesday, October 30, 2024
HomeTechnologySoftwareUsing the Strangler Fig with Mobile Apps

Using the Strangler Fig with Mobile Apps

Date:

Related stories

spot_imgspot_img


In this article we aim to show why taking an incremental approach to
legacy mobile application modernization can be preferable to the classical
‘rewrite from scratch’ methodology. Thoughtworks has the benefit of working with
large enterprise clients that are dependent on their in-house mobile
applications for their core business. We see many of them asking their
applications to do more and evolve faster, while at the same time, we see an
increasing rejection of reputationally damaging high risk releases.

As a solution, this article proposes alternative methods of legacy
modernization that are based in Domain Driven Design and hinge on the
application of the Strangler Fig pattern. While these concepts are far from
new, we believe that their usage in mobile applications are novel. We feel
that despite incurring a larger temporary overhead from their usage, this is
an acceptable tradeoff. We assert how the methodology is used to combat the
aforementioned attitudinal shifts in legacy mobile application development
while gaining a platform to lower risk and drive incremental value
delivery.

We discuss how this works in theory, diving into both the architecture
and code. We also recount how this worked in practice when it was trialled on
a large, legacy mobile application at one of Thoughtworks’ enterprise
clients. We highlight how the pattern enabled our client to rapidly build,
test and productionize a modernized subset of domain functionalities inside
an existing legacy application.

We move on to evaluate the effectiveness of the trial by highlighting the business
facing benefits such as a signficantly faster time to value and a 50% reduced median cycle
time. We also touch on other expected benefits that should be used to
measure the success of this methodology.

The Problem with Mobile Legacy Modernization

As applications age and grow, they tend to deteriorate both in quality
and performance. Features take longer to get to market while outages
and rolled back releases become more severe and frequent. There is a
nuanced complexity to be understood about the reasons why this
occurs both at the code and organizational level.
To summarize though, at some point, an
organization will grow tired of the poor outcomes from their
software and start the process of legacy replacement. The decision
to replace may be made based on multiple factors, including (but not limited to)
cost/benefit analysis, risk analysis, or opportunity cost. Eventually a legacy modernization strategy will be chosen.
This will be dependent on the organization’s attitude to risk. For
example, a complex, high availability system may demand a more
incremental or interstitial approach to legacy
replacement/displacement than a simpler, less business critical one.

In the case of mobile application modernization, those decisions have
in recent memory been reasonably clear cut. A mobile application was
often designed to do an individual thing- Apple’s “There’s an app for
that” still rings out loud and clear in people’s minds 15 years after
the initial batch of advertisements. That message was one that was taken
to heart by organizations and startups alike: If you need to do
something, write an app to do it. If you need to do something else, write
another app to do that.
This example struck me when I was
pruning the apps on my phone a couple of years ago. At the time I noticed I
had several apps from the manufacturer of my car; an older one and a newer
one. I also had two apps from my bank; one showed my checking account,
another that analyzed and illustrated my spending habits. I had three apps
from Samsung for various IoT devices, and at least two from Philips that
controlled my toothbrush and light bulbs. The point I’m laboring here is
that a mobile application was never allowed to get so complicated,
that it couldn’t be torn down, split out or started from scratch again.

But what happens when this isn’t the case? Surely not all apps are
created equal? Many believe that the mobile experience of the future
will be centered around so-called
“super-apps”
; apps where you can pay, socialize, shop, call,
message, and game, all under one application. To some degree this has
already happened in China with “do-everything” applications like
‘WeChat’ and ‘AliPay’- we see the mobile device and its operating
system as more of a vehicle to allow the running of these gigantic
pieces of software. Comments from industry indicate a realization
that the West
is not quite as far along as China in this regard
. But while not
at the super-app, there is no doubt that complexity of the mobile
app experience as a whole has increased significantly in recent
years. Take the example of YouTube, when first installed, back in
the early 2010’s, the application could play videos and not much
else. Opening the application today one is presented with “Videos”
and “Shorts”, a news feed, controllable categories, subscriptions,
not to mention a content editing and publishing studio. Similarly
with the Uber app, the user is asked if they want to order food.
Google Maps can show a 3D view of a street and Amazon now recommends
scrollable product-recommendation mood boards. These extra features
have certainly enriched a user’s experience but they also make the
traditional build, use, rebuild technique much more difficult.

This difficulty can be explained by considering some of the existing
common problems of mobile application development:

  • Massive View Controllers/Activities/Fragments
  • Direct manipulation of UI elements
  • Platform specific code
  • Poor Separation of Concerns
  • Limited Testability

With discipline, these problems can be managed early on. However, with
a large application that has grown chaotically inline with the business it
supports, incremental change will be difficult regardless. The solution then, as
before, is to build new and release all at once. But what if you only want
to add a new feature, or modernize an existing domain? What if you want to
test your new feature with a small group of users ahead of time while
serving everyone else the old experience? What if you’re happy with your
app store reviews and don’t want to risk impacting them?

Taking an incremental approach to app replacement then is the key to
avoiding the pitfalls associated with ‘big bang releases’. The Strangler
Fig pattern
is often used to rebuild a legacy application in
place: a new system is gradually created around the edges of an old
one through frequent releases. This pattern is well known, but
not widely used in a mobile context. We believe the reason for this is that there are several prerequisites that need to be in
place before diving headfirst into the pattern.

In their article on Patterns
of Legacy Displacement
, the authors describe four broad
categories (prerequisites) used to help break a legacy problem into
smaller, deliverable parts:

  1. Understand the outcomes you want to achieve
  2. Decide how to break the problem up into smaller parts
  3. Successfully deliver the parts
  4. Change the organization to allow this to happen on an ongoing
    basis

Only in the third point, can we envisage the invocation of the Strangler Fig
pattern. Doing so without an understanding of why, what or how it might
continue in the future is a recipe for failure.

Going forward, the article charts how Thoughtworks was able to help one
of its enterprise clients expand its existing mobile legacy modernization
efforts into a successful experiment that demonstrated the value behind
the use of the Strangler Fig pattern in a mobile context.

Satisfying the Prerequisites

At this point, it seems appropriate to introduce the client that
inspired the writing of this article – a globally distributed business
with an established retail organization that had embraced mobile
applications for many years. Our client had realized the benefits an
app brought to provide a self-service experience for their
products. They had quickly expanded and developed their app domains to allow millions
of customers to take full advantage of all the products they sold.

The organization had already spent a significant amount of time and
effort modernizing its mobile applications in its smaller
sub-brands. Responding to a lack of reuse/significant duplication of
efforts, high
cognitive load
in app teams and slow feature delivery, the
organization chose a mobile technology stack that leveraged a
Modular Micro-app architecture. This strategy had been largely
successful for them, enabling proliferation of features common to
the organization (e.g. ‘login/registration/auth’ or ‘grocery shopping’)
across different brands and territories, in a fraction of the time it
would have taken to write them all individually.

The diagram above is a simplified representation of the modular
architecture the organization had successfully implemented. React
Native was used due to its ability to entirely encapsulate a
domain’s bounded context within an importable component. Each
component was underpinned by its own backend
for frontend (BFF)
that came with the infrastructure as code to
instantiate and run it. The host apps, shown above as UK and US,
were simply containers that provided the app specific configuration
and theming to the individual micro-apps. This ‘full slice’ of
functionality has the advantages of both allowing re-use and
reducing complexity by abstracting application domains to micro-apps
managed by individual teams. We speak in depth about the results of
this architecture in the already referenced article on ‘Linking
Modular Architecture’
.

As touched upon earlier, the organization’s mobile estate was made up of
a number of smaller sub-brands that served similar products in other
territories. With the modular architecture pattern tried and tested, the
organization wanted to focus efforts on its ‘home-territory’ mobile
application (serving its main brand). Their main mobile app was much
larger in terms of feature richness, revenue and user volumes to that of
the sub brands. The app had been gaining features and users over many
years of product development. This steady but significant growth had
brought success in terms of how well-regarded their software was on both
Google and Apple stores. However, it also started to show the
characteristic signs of deterioration. Change frequency in the application
had moved from days to months, resulting in a large product backlog and
frustrated stakeholders who wanted an application that could evolve as
fast as their products did. Their long release cycle was related to risk
aversion: Any outage in the application was a serious loss of revenue to
the organization and also caused their customers distress due to the
essential nature of the products they sold. Changes were always tested
exhaustively before being put live.

The organization first considered a rewrite of the entire application
and were shocked by the cost and duration of such a project. The potential
negative reception of a ‘big bang’ new release to their app store
customers also caused concerns in the levels of risk they could accept.
Suggestions of alpha and beta user groups were considered unacceptable
given the huge volumes of users the organization was serving. In this
instance, a modernization effort similar to that seen in their sub-brands
was believed to be of considerably higher cost and risk.

Thoughtworks suggested an initial proof of concept that built on the
successes of the reusability already seen with a modular
architecture. We addressed the organization’s big bang risk aversion
by suggesting the Strangler
Fig pattern
to incrementally replace individual domains. By
leveraging both techniques together we were able to give the
organization the ability to reuse production-ready domains from
their modernized mobile apps inside their legacy app experience. The
idea was to deliver value into the hands of customers much sooner
with less duplication than in a full rewrite. Our focus was not on
delivering the most beautiful or cohesive full app experience (-not
quite yet anyway). It was about obtaining confidence both in the
stability of the iterative replacement pattern and also in how well
the new product was being received. These pieces of information
allowed the organization to make more informed product decisions
early on in the modernization process. This ensured the finished product
had been extensively used and molded by the actual end users.

Strangler Fig and Micro-apps

So how far did we get with the proof of concept and more importantly
how did we actually do this? Taking the learnings from Modular Micro-app
architecture (described above), we theorized the design to be as follows:

The initial state of the application involved the identification of
domains and their navigation routes (Decide how to break the problem into
smaller parts)
. We focused our efforts on finding navigation entry points
to domains, we called them our ‘points of interception’. Those familiar
with mobile application development will know that navigation is generally
a well encapsulated concern, meaning that we could be confident that we
could always direct our users to the experience of our choosing.

Once we identified our ‘points of interception’, we selected a domain
for incremental replacement/retirement. In the example above we focus on
the Grocery domain within the existing application. The ‘new‘ Grocery domain,
was a micro-app that was already being used within the sub-brand apps. The
key to implementation of the Strangler Fig pattern involved embedding an
entire React Native application inside the existing legacy application.
The team took the opportunity to follow the good modularity practices that
the framework encourages and built Grocery as an encapsulated component. This
meant that as we added more domains to our Strangler Fig Embedded
Application, we could control their enablement on an individual level.

As per the diagram, in the legacy app, Grocery functionality was
underpinned by a monolithic backend. When we imported the New Grocery
Micro-app, it was configured to use that same monolithic backend. As
mentioned previously, each micro-app came with its own Backend for
Frontend (BFF). In this instance, the BFF was used as an anti-corruption
layer; creating an isolating layer to maintain the same domain model as
the frontend. The BFF talked to the existing monolith through the same
interfaces the legacy mobile application did. Translation between both
monolith and micro-app happened in both directions as necessary. This
allowed the new module’s frontend not to be constrained by the legacy API
as it developed.

We continued the inside out replacement of the old application by
repeating the process again on the next prioritized domain. Although out
of scope for this proof of concept, the intention was that the process
shown be repeated until the native application is eventually just a shell
containing the new React Native application. This then would allow the removal of the
old native application entirely, leaving the new one in its place. The new
application is already tested with the existing customer base, the
business has confidence in its resilience under load, developers find it
easier to develop features and most importantly, unacceptable risks
associated with a typical big bang release were negated.

Diving Deeper…

So far we’ve presented a very broad set of diagrams to
illustrate our Mobile Strangler Fig concept. However, there are
still many
outstanding implementation-focused questions in order to take theory
into
practice.

Implanting the Strangler Fig

A good start might be, how did we abstract the complexity of
building both native and non-native codebases?

Starting with the repository structure, we turned our original native
application structure inside out. By inverting the control
of the native application to a React Native (RN) application
we avoided significant duplication associated with nesting
our RN directory twice inside each mobile operating system’s
folder. In fact, the react-native init default
template gave a structure to embed our iOS and Android
subfolders.

From a developer perspective, the code was largely unchanged. The
legacy application’s two operating-system-separated teams were able to
target their original directories, only this time it was within a single
repository. The diagram below is a generalized representation (that is,
applicable to both iOS and Android) of the current pipeline from the
Client as we understood:

Bi-Directional Communication using the Native Bridge

We’ve already touched on navigation with our previously mentioned
‘points of interception’. It is worth looking deeper into how we
facilitated communication and the transfer of control between native and
React Native as it would be easy to oversimplify this area.

The React
Native ‘Bridge’
enables communication between both
worlds. Its purpose is to serve as the message queue for
instructions like rendering views, calling native functions,
event handlers, passing values etc. Examples of
properties passed across the bridge would be isCartOpen
or sessionDuration. While an example of a bridge
function call might be js invocations of the device’s native geolocation
module
.

The diagram above also references the concept of a ‘React Native
Micro App’. We introduced this concept earlier in the article when we
described our app in terms of journeys. To recap though, a micro-app is a self-contained
encapsulation of UI and functionality related to a single
domain. A React Native app may be made up of many micro-apps
similar to the micro
frontend pattern
. In addition to those advantages we have already discussed, it also allows us to have a greater
degree of control over how our Strangler Fig application
grows and is interacted with. For example, in a situation
where we have more confidence in one of our new journeys
than another we are afforded the option to divert a larger
proportion of traffic to one micro-app without impacting
another.

Bringing both concepts together, we utilized the bridge to
seamlessly move our users back and forth across experiences.
The ability to pass information allowed us to preserve any
immediate state or action from the UI that needed to
persevere across experiences. This was particularly useful
in our case as it helped us to decouple domains at
appropriate fracture points without worrying whether we
would lose any local state when we crossed the bridge.

Handling Sensitive Data

So far we’ve discussed moving between legacy and new codebases as
atomic entities. We’ve touched on how local state can be
shared across the bridge, but what about more sensitive
data? Having recently replaced their login and registration (auth)
process in their other customer-facing React Native apps
with a modular, configurable, brand agnostic one, the client
was keen for us to reuse that experience. We set ourselves
the task of integrating this experience as an
initial demonstration of the Strangler Fig pattern in
action.

We leveraged the techniques already discussed to implant the
Strangler Fig: i.e. the new authentication journey on the
React Native side. When a customer successfully logged in or
registered, we needed to ensure that if they moved away from
the new experience (back into the legacy journey), their
authentication status was preserved no matter where they
were.

For this, we utilized the native module code calling side of the
bridge. The diagram above explains how we achieved this by
using a React Native library that served as a wrapper to
save authentication data to the Android
EncryptedSharedPreferences or iOS Keychain after a
successful login. Due to the flexible structure of the data
inside the keystore, it allowed us to seamlessly share the
(re)authentication process irrespective of whether
the user was in the native or non-native experience. It also
gave us a pattern for the secure sharing of any sensitive
data between experiences.

Regression Testing at Domain Boundaries

An important part of a cutover strategy is the ability to know
from any vantage point (in our case, different teams working within the same app) whether a change made affected the
overall functionality of the system. The embedded app
pattern described above presents a unique challenge in this
regard around scalable testability of a multi-journey
experience. Moreover one that is managed by multiple teams
with numerous branching paths.

UserNative App(maintained byNative Team)React Native (RN) BridgeRN AuthMicro-app(maintained by RN Team)RN Grocery ShoppingMicro-app(maintained by RN Team) Opens App Native app requests theinitialization ofRN Auth micro-app RN Auth micro-appinitializeUser is presented theRN Auth micro-appUser logs in usingRN Auth micro-app User’s credentials is sentto the micro-app for processing Request to initializeRN Grocery Shoppingmicro-app Initialize request RN Grocery Shoppingmicro-app initialized User is presented theRN GroceryShoppingmicro-appMicro-app processescredentials & resultsto successful authentication Initializes RN Grocery shopping micro-appbecause of a feature flag

The interaction diagram above shows an example journey flow
within the embedded app. One thing to notice is the amount
of branching complexity across a journey that is carrying
out just two concurrent experiments. We speak more on accidental complexity later in this section.

The test
pyramid
is a well known heuristic that recommends a
relationship between the cost of a test (maintenance and
writing) and its quantity in the system. Our client had kept
to the test pyramid and we found unit, subcutaneous and
journey-centric UI-driving tests when we examined their
code. The solution therefore was to continue to follow the
pattern: Expanding the number of tests across all layers and
also extending the suite of journey tests to incorporate the
jumping in and out of our embedded Strangler Fig app. But
there was a potential problem, ownership. We realized
that it would be unreasonable to tie the success of another
team’s build to code they did not write or were in control of.
We therefore proposed the following test 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

On the last table row, by contract we simply mean:

If I interact with the bridge interface a particular way, I
expect a specific event to fire

For Native to RN interactions, these contracts act as blueprints
for micro-apps and enable unit testing with mocks. Mocks
simulate the behavior of the micro-app, ensuring it utilizes
the required context correctly.

The other way around (RN to Native) was similar. We identified
the Native functionality we wished to call through the
Bridge. RN then provided us with an object called
NativeModules which, when mocked, allowed us to assert
against the resulting context.

Defining these boundaries of responsibility meant that we could
limit the ‘regression-related’ cognitive load on teams through
‘hand-off’ points without compromising on overall app test
coverage.

This strategy was largely well received by both the native and
non-native teams. Where we did run into friction was the
complexity behind the implementation of the contract tests
across the bridge. The team running the legacy application
simply did not have the bandwidth to understand and write a
new category of tests. As a compromise, for the duration of
the PoC, all contract tests were written by the React Native
team. From this we learned that any interstitial state
required thought to be paid to the developer experience. In
our case, simply layering complexity to achieve our goals
was only part of the problem to be solved.

Creating the Experiment

Bringing everything together to form an experiment was the last
hurdle we had to overcome. We needed a means to be able to
demonstrate measurable success from two different
experiences and also have an ability to quickly backout and
revert a change if things were going wrong.

The organization had an existing integration with an
experimentation tool, so out of ease, we chose it as our
tool for metric capture and experiment measurement. For experiment
user selection, we decided device level user selection (IMEI
number) would be more representative. This was due to the
potential for multiple device usage across a single account
skewing the results.

We also utilized the feature
flagging component of the experimentation tool to allow us to ‘turn off’ the experiment (revert to
native app only) without the need for a release; greatly
reducing the time taken to recover should any outage occur.

We’re releasing this article in installments. The next and final
installment will describe the results of this experiment: how it altered
time to value and cycle time.

To find out when we publish the next installment subscribe to this
site’s
RSS feed, or Martin’s feeds on
Mastodon,
LinkedIn, or
X (Twitter).






Source link

Latest stories

spot_img