<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Scott Berrevoets - Architecture &amp; Infrastructure</title><link href="https://scottberrevoets.com/" rel="alternate"/><link href="https://scottberrevoets.com/feeds/architecture-infrastructure.atom.xml" rel="self"/><id>https://scottberrevoets.com/</id><updated>2026-08-11T00:00:00-07:00</updated><entry><title>The foundations of a mobile platform</title><link href="https://scottberrevoets.com/2026/08/11/the-foundations-of-a-mobile-platform/" rel="alternate"/><published>2026-08-11T00:00:00-07:00</published><updated>2026-08-11T00:00:00-07:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2026-08-11:/2026/08/11/the-foundations-of-a-mobile-platform/</id><summary type="html">&lt;p&gt;In larger teams or companies, most people agree that the mobile platform is the
shared infrastructure an app and its features are built upon. In earlier stages,
that might simply mean "shared code and helper functions", but when the app and
team start scaling, the platform deserves a lot more …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In larger teams or companies, most people agree that the mobile platform is the
shared infrastructure an app and its features are built upon. In earlier stages,
that might simply mean "shared code and helper functions", but when the app and
team start scaling, the platform deserves a lot more investment and
consideration. The scope of the platform broadens very quickly, and its
foundations have a big influence on the engineering velocity of all product
teams and the overall reliability of the app in the hands of users.&lt;/p&gt;
&lt;p&gt;For a good, scalable platform, a more complete definition is &lt;strong&gt;the underlying
systems that enable product teams to build, run, observe, and distribute their
products&lt;/strong&gt;. The verbs above do a lot of heavy lifting in terms of what systems
they consist of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build&lt;/strong&gt;: architecture, CI/CD, build and lint tools, IDE, developer
experience, agent tooling, guidance, and context&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run&lt;/strong&gt;: networking, persistence, authentication, testing frameworks, and any
app/domain-specific systems such as audio playback/voice recording or the user
location and mapping frameworks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observe&lt;/strong&gt;: analytics, logging, crash/error reporting, general observability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distribute&lt;/strong&gt;: release/QA process, experimentation, bug reporting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The size of the platform scales with the size of the codebase, team, and
operational complexity. Many apps start simple with very basic functionality to
test out an idea, but if the idea catches on and the app scales, the feature set
grows, more people start contributing, and distribution and monitoring become
more complicated.&lt;/p&gt;
&lt;p&gt;Most apps will have some of the systems I mentioned above, however simple, and
as the app's complexity or functionality set grows, so does the platform. Even
if there's no explicit task to build out some part of the platform, developers
tend to extract reusable code so it can be shared across features or even across
apps which is the start of the platform. &lt;/p&gt;
&lt;h2 id="mobile-is-even-more-complicated"&gt;Mobile is even more complicated&lt;/h2&gt;
&lt;p&gt;These systems are not uniquely important on mobile, but they are
&lt;em&gt;especially&lt;/em&gt; important there. Native apps are downloaded once and run on the
end user's device, outside of the engineering team's controlled environment.
This means network conditions, hardware capabilities, other running apps,
and OS versions and settings all vary from user to user. New app versions
don't propagate to all users right away, but more gradually over time.
Observability into the user's hardware environment is critical, yet more
difficult to implement.&lt;/p&gt;
&lt;p&gt;Getting the mobile platform right means accounting for all these variable
factors as best as possible. It means the design system is implemented in both
dark and light mode. It means the error reporting framework attaches relevant
context about the user and device to error reports. It means retrying network
requests for a reasonable amount of time and then failing gracefully if the
network is bad. It means code is easy to test because bugs in production will
stay around for a while.&lt;/p&gt;
&lt;h2 id="the-impact-of-a-good-platform"&gt;The impact of a good platform&lt;/h2&gt;
&lt;p&gt;Platform development becomes increasingly important as the app scales. Apps
that scale that much usually are popular and have a bigger company built
around the app, or there was already a company that's now building an app.&lt;/p&gt;
&lt;p&gt;The platform is the foundation of the whole app, which means the quality of the
app is, to a large degree, defined by the quality of the platform:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The quality of the design system decides the quality of the UI in terms of
  consistency, usability, and accessibility&lt;/li&gt;
&lt;li&gt;A fast networking layer with good retry and cancelation policies lets the user
  get their tasks done fast and without network frustration&lt;/li&gt;
&lt;li&gt;Architectural patterns decide how testable and reliable the app is&lt;/li&gt;
&lt;li&gt;Observability into runtime behavior and edge cases make it easier to catch
  and fix bugs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The quality of these systems has a &lt;a href="https://scottberrevoets.com/2025/07/02/flywheel-of-tech-debt/"&gt;compounding effect&lt;/a&gt;, both good and bad. A
bad platform gets in the way, requires constant maintenance and firefighting,
and brings a lot of uncertainty. Engineers spend a lot of time answering
questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What networking client do I use? &lt;/li&gt;
&lt;li&gt;Is there a way to easily mock the user service?&lt;/li&gt;
&lt;li&gt;How do I know my feature works as expected after rollout?&lt;/li&gt;
&lt;li&gt;Where in the codebase does my feature fit?&lt;/li&gt;
&lt;li&gt;Can I build this UI using existing components?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good platform has clear answers to these questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use the standard networking client unless you need one of the documented
  unsupported cases&lt;/li&gt;
&lt;li&gt;Mocking services works the same way everywhere&lt;/li&gt;
&lt;li&gt;Observability is standardized and built-in; feature-specific observability
  uses the same logging, analytics, and alerting stack&lt;/li&gt;
&lt;li&gt;Module boundaries, naming, and project organization make it easy to
  navigate the codebase and find relevant code&lt;/li&gt;
&lt;li&gt;Feature UI starts from the design system and uses its components directly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Developers have high trust in this platform because it accelerates development
and operational velocity while improving feature quality.&lt;/p&gt;
&lt;p&gt;In modern-day engineering, agentic workflows heavily depend on the strength
of the platform, as LLMs do better in codebases with consistent patterns, clear
constraints, and tests to validate changes. Agents will repeat the patterns they
see, so the stronger they are the stronger they will continue to be. If there
are multiple ways to do the same thing, the agent has to pick which one to
choose, or it introduces a third, hybrid pattern.&lt;/p&gt;
&lt;p&gt;Existing documentation can double as agent guidance and requires less explicit
context. It can be the difference between a simple "implement a GET request to
the users endpoint" prompt and a much more specific one that requires the
engineer to type more and know and remember all the intricacies of the
networking client.&lt;/p&gt;
&lt;h2 id="why-is-building-a-platform-difficult"&gt;Why is building a platform difficult?&lt;/h2&gt;
&lt;h3 id="industry-standards-are-limited"&gt;Industry standards are limited&lt;/h3&gt;
&lt;p&gt;Apple and Google as platform vendors provide the tools necessary to build and
ship an app, but their primary audience is smaller, often indie, app developers.
They provide very little tooling specific to larger teams: CI, observability and
code coverage dashboards, linters, blessed architectures, migration efforts,
etc. are either too limited or don't exist at all.&lt;/p&gt;
&lt;p&gt;Some third-party tooling exists, but is often the under-rewarded effort of open
source developers that can't give this work the time and attention it deserves.
Other tools become paid if a company finds a way to monetize them. The platform
is a moving target with major updates every year and many smaller ones
throughout, so keeping up takes time.&lt;/p&gt;
&lt;p&gt;Some of these systems also can't be built generically, like a design system and
its fonts, colors, spacing, etc., or need to be chosen across the stack like
experimentation or analytics frameworks.&lt;/p&gt;
&lt;h3 id="it-requires-good-judgment"&gt;It requires good judgment&lt;/h3&gt;
&lt;p&gt;Building the platform well requires good judgment and experience, and ideally
experience in exercising good judgment. Generally this means more senior
engineers that have seen both good and bad, with technical depth in a lot of
different areas. Platform work means understanding when to duplicate or build an
abstraction, what kind of testing is the most effective, and what migrations are
worth doing and when, which doesn't come naturally to most engineers. I still
often have to remind myself that platform engineering in many ways is a very
different skill from product engineering.&lt;/p&gt;
&lt;h3 id="changing-course-is-expensive"&gt;Changing course is expensive&lt;/h3&gt;
&lt;p&gt;When dozens of features rely on a system, you can't just remove that system or
replace it with something else. It requires a &lt;a href="https://scottberrevoets.com/2022/11/15/migration-strategies-in-large-codebases/"&gt;code migration&lt;/a&gt;, which could take
months or years of consistent effort. Many teams are not willing to do that and
would rather live with the old system.&lt;/p&gt;
&lt;h3 id="its-a-long-term-bet"&gt;It's a long-term bet&lt;/h3&gt;
&lt;p&gt;It's a long play: building out each system takes time, and teams don't
immediately see the benefits of the work. The real compounding effect happens
once many of these systems are adopted widely. Building a solid logging system
does nothing until most features use the system.&lt;/p&gt;
&lt;h3 id="its-a-people-y-job"&gt;It's a "people-y" job&lt;/h3&gt;
&lt;p&gt;Doing this work requires understanding team dynamics and other &lt;a href="https://scottberrevoets.com/2022/09/28/human-factors-in-choosing-technologies/"&gt;human factors&lt;/a&gt;.
Knowing what battles to pick, how to educate people, working with others to find
the best solution for them, reiterating ideas and visions, etc. is critical for
this work. Other developers are the customers, and keeping the customer happy is
important.&lt;/p&gt;
&lt;h2 id="who-should-own-the-platform"&gt;Who should own the platform?&lt;/h2&gt;
&lt;p&gt;Platform ownership is a long-term, continuous effort. This usually means a
dedicated team as the app scales. Org structure and size doesn't really
matter: at Speak I'm the only iOS platform engineer, at Lyft I led an org of
3 teams.&lt;/p&gt;
&lt;p&gt;The development and maintenance of a platform usually undergoes multiple
iterations as the tech, industry, and OS evolve over the years. That upkeep is
much easier to execute on when one team develops a vision, builds the expertise,
and owns the development than if individual pieces are owned by different teams.&lt;/p&gt;
&lt;p&gt;A platform whose quality depends on the deadlines, narrow viewpoints, and
political pressure of a product-oriented team is less likely to remain of high
quality as it evolves. That team might not be able to prioritize any ongoing
maintenance, doesn't look at its development holistically, or dissolves entirely
leaving the code more or less unowned.&lt;/p&gt;
&lt;p&gt;The outcome tends to be better if a more neutral developer, with good
connections to all the teams is responsible. That developer is then in a better
position to come up with scalable solutions that work across the board instead
of solving a single use case.&lt;/p&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Platform Engineering"/><category term="Architecture"/><category term="Developer Experience"/></entry><entry><title>Managing code deprecations on iOS</title><link href="https://scottberrevoets.com/2025/08/20/managing-code-deprecations-on-ios/" rel="alternate"/><published>2025-08-20T00:00:00-07:00</published><updated>2025-08-20T00:00:00-07:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2025-08-20:/2025/08/20/managing-code-deprecations-on-ios/</id><summary type="html">&lt;p&gt;A great manager I once worked with had been at Google for a while and poked fun
at an aspect of its engineering culture: any given service was either deprecated
or not yet ready for adoption. I spend a lot of my time working on mobile
platform development and over …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A great manager I once worked with had been at Google for a while and poked fun
at an aspect of its engineering culture: any given service was either deprecated
or not yet ready for adoption. I spend a lot of my time working on mobile
platform development and over the years have been exposed to both sides of this:
needing to get rid of code that's deprecated in new OS versions, and deprecating
technologies many internal teams depend on.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vendor deprecations&lt;/strong&gt; usually involve the platform vendor releasing new APIs
  or technologies that replace older ones and they want their developer base to
  put in the work to use this new tech&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Internal deprecations&lt;/strong&gt; come with a lot of non-technical work and a long
  migration path, often in the order of quarters or years, on top of the
  technical challenge and often ugly tradeoffs in making the old and systems
  compatible during the transition period&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both types of deprecations are a way of life for platform engineering, and yet
on Apple platforms we have very little tooling available to make these easier.
In fact, the only real tool we have is &lt;code&gt;@available()&lt;/code&gt; to mark a type as having
been deprecated:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deprecated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;renamed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;myNewFunction()&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;myFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;// deprecated and renamed to myNewFunction&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iOS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deprecated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;26.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;myFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;// deprecated starting in iOS 26&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deprecated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Please use myOtherNewFunction instead&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;myOtherFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;// deprecated with a custom warning message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Apple uses this pattern extensively throughout their frameworks and every new OS
version introduces more deprecation annotations exactly like the examples above.
This makes raising the deployment target in a sufficiently large project a much
bigger initiative than it might seem at first, because suddenly you're facing a
large number of deprecation warnings.&lt;/p&gt;
&lt;p&gt;Handling the deprecation of a single class can in extreme cases be a project on
its own. If you're looking to raise the deployment target to use the latest
SwiftUI APIs, it can be a bummer to first have to spend a bunch of time
resolving deprecation warnings, especially when &lt;code&gt;warnings-as-errors&lt;/code&gt; is on and
the app doesn't even build.&lt;/p&gt;
&lt;p&gt;Since &lt;code&gt;@available&lt;/code&gt;'s only real logic is gating on OS and Swift versions, it
isn't helpful for internal deprecations. If I write a new database class that
replaces an old one that's used in 50+ places, I don't know what OS version to
specify because it's not related to OS-specific system APIs. It's more important
to think about how to manage the existing call sites, which in most cases means
accepting the 50+ instances, ensuring it doesn't grow any further, and working
with the team to migrate them over time.&lt;/p&gt;
&lt;p&gt;Annotating the old class with &lt;code&gt;@available(*, deprecated)&lt;/code&gt; would immediately
cause build errors, so that's not an option. At Lyft we often resorted to the
ugly workaround of renaming the old class to &lt;code&gt;DeprecatedDatabase&lt;/code&gt; or even
&lt;code&gt;DEPRECATEDDatabase&lt;/code&gt;. This worked surprisingly well, but it also very clearly
highlights a gap in tooling.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0443-warning-control-flags.md"&gt;SE-0443: Precise Control Flags over Compiler Warnings&lt;/a&gt; gives developers more
granular control over compiler warnings, even mentioning the deprecation use
case specifically. In short, there are now build settings that enable upgrading
and downgrading warnings of a specific type to and from errors. The proposal
gives this example to turn warnings-as-errors on except for deprecations:
&lt;code&gt;-warnings-as-errors -Wwarning Deprecated&lt;/code&gt;. This leaves &lt;code&gt;warnings-as-errors&lt;/code&gt; on
for everything except deprecations.&lt;/p&gt;
&lt;p&gt;It's an OK first step, but not really sufficient:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There might be multiple deprecations happening at once, but you can only
   specify whether to enable or disable deprecation warnings as a whole, not per
   type&lt;/li&gt;
&lt;li&gt;Even if per-type warning disables were available, that still means it's too
   easy to accidentally or unknowingly introduce new call sites of a deprecated
   type&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The most ideal solution is bringing back a relic from the Objective-C past: C
compiler directives. In C-based languages, &lt;code&gt;#pragma&lt;/code&gt; blocks could be used to
ignore specific instances of a warning:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;C&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;#pragma GCC diagnostic ignored &amp;quot;-Wdeprecated-declarations&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;myDeprecatedFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// no warning&lt;/span&gt;

&lt;span class="cp"&gt;#pragma GCC diagnostic pop&lt;/span&gt;

&lt;span class="n"&gt;myDeprecatedFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// warning: myDeprecatedFunction is deprecated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Although the syntax is clearly not very "2025", the idea is still good and
exactly what should happen to improve this bit of API tooling. The deprecation
warning on the call site that's "wrapped" in the &lt;code&gt;diagnostic&lt;/code&gt; directive would be
ignored, and introducing a new caller of &lt;code&gt;myDeprecatedFunction&lt;/code&gt; generates a
warning (or more ideally, a build error). You can choose to wrap that new call
in the same &lt;code&gt;#pragma&lt;/code&gt; block, but then it's an explicit choice and not something
the developer was just unaware of.&lt;/p&gt;
&lt;p&gt;Incidentally, having multiple levels of granularity is also exactly how e.g.
SwiftLint and linters on other platforms work: you can disable rules entirely,
per-file, per code-section, per-line, etc. It's too bad SE-443 didn't go that
far, but discussions in the &lt;a href="https://forums.swift.org/t/se-0443-precise-control-flags-over-compiler-warnings/74116"&gt;developer forums&lt;/a&gt; have pointed out these same
problems so hopefully those will be considered in future improvements.&lt;/p&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Technical Debt"/><category term="Platform Engineering"/></entry><entry><title>Flywheel of tech debt</title><link href="https://scottberrevoets.com/2025/07/02/flywheel-of-tech-debt/" rel="alternate"/><published>2025-07-02T00:00:00-07:00</published><updated>2025-07-02T00:00:00-07:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2025-07-02:/2025/07/02/flywheel-of-tech-debt/</id><summary type="html">&lt;p&gt;Tech debt is often compared to financial debt, in that you're borrowing
development time now to pay it off with more development time later. When
"later" comes around, the development time needed to fix the issues introduced
earlier is too high so the new thing is also not built "correctly …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Tech debt is often compared to financial debt, in that you're borrowing
development time now to pay it off with more development time later. When
"later" comes around, the development time needed to fix the issues introduced
earlier is too high so the new thing is also not built "correctly", and the
pattern repeats itself: tech debt begets tech debt.&lt;/p&gt;
&lt;p&gt;A few years ago I came across an article about the &lt;a href="https://technology.riotgames.com/news/taxonomy-tech-debt"&gt;Taxonomy of Tech Debt&lt;/a&gt;,
which talks about the contagion of tech debt: the easier a piece of tech debt
spreads, the higher its impact because fixing that fragile code means
preemptively fixing it in future cases as well.&lt;/p&gt;
&lt;p&gt;A more extreme version of this is a &lt;strong&gt;flywheel of tech debt&lt;/strong&gt;&amp;trade;: tech debt
that doesn't just expand linearly because of a high contagion factor, but that
multiplies at an excessive rate. This tends to happen in one specific part of
the codebase: the infrastructure. Any libraries, frameworks, architectures,
foundational patterns, etc. with tech debt have this flywheel and once it's
rotating it's very difficult to stop.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With a sufficient number of users of an API, it does not matter what you
promise in the contract: all observable behaviours of your system will be
depended on by somebody.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://xkcd.com/1172/"&gt;Hyrum's Law&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hyrum's Law is the primary reason why it's so hard to remove any tech debt from
infrastructure code: even if the tech debt is internal to the architecture to
account for one edge case, the behavior is there and &lt;em&gt;will&lt;/em&gt; be depended on by
somebody. If the tech debt is more severe and affects the API, the flywheel goes
brrr and now &lt;em&gt;requires&lt;/em&gt; all consumers of that API to incur the tech debt as
well, unless they specifically understand (how) to avoid it. &lt;/p&gt;
&lt;p&gt;What's worse is that after some time passes and people are using these APIs,
it's much more difficult to remove because it also requires removing it at all
the call sites that API. For a large team/codebase, this requires a lot of
&lt;a href="https://scottberrevoets.com/2022/11/15/migration-strategies-in-large-codebases/"&gt;coordination between teams&lt;/a&gt; and can take years of time and effort. One team
that doesn't pull its weight impacts everyone else.&lt;/p&gt;
&lt;p&gt;This is ironic because in many situations that foundational layer with good
abstractions and nice developer ergonomics is specifically created to &lt;em&gt;avoid&lt;/em&gt;
tech debt. The more this happens, the more brittle the foundations get until we
get to the &lt;a href="https://xkcd.com/2347/"&gt;other xkcd&lt;/a&gt;. As such, I tend to advocate for moving the fragile code
downstream, to the feature code as much as possible. This reduces chances of it
impacting other features, even if that means the fragile code is worse or more
risky.&lt;/p&gt;
&lt;p&gt;The cost of fragile infrastructure code is so high because that code is
generally long-lived, where the feature may be scrapped before it even launches.
The team that introduces this code is now also directly responsible for it
without impacting everybody else in the process. And, if the tech debt finally
becomes too painful to deal with, fixing it is much simpler and easier to roll
out than a fundamental change in the foundational layer of an app.&lt;/p&gt;
&lt;p&gt;In the past I reluctantly accepted a hack here and there in some foundational
libraries, but I intend to push back harder going forward. Spending years
working out a single parameter from a public method in some part of our
architecture is a valuable lesson learned...&lt;/p&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Technical Debt"/><category term="Architecture"/><category term="Platform Engineering"/></entry><entry><title>Art of the state</title><link href="https://scottberrevoets.com/2025/06/02/art-of-the-state/" rel="alternate"/><published>2025-06-02T00:00:00-07:00</published><updated>2025-06-02T00:00:00-07:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2025-06-02:/2025/06/02/art-of-the-state/</id><summary type="html">&lt;p&gt;&lt;code&gt;@State&lt;/code&gt; plays a prominent role in any SwiftUI app, and for good reason: app
state is what ultimately drives the UI and how the user interacts with your app.
This has always been true, but SwiftUI embraces this reality with specific APIs
and a unidirectional data flow. UIKit-based apps or …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;code&gt;@State&lt;/code&gt; plays a prominent role in any SwiftUI app, and for good reason: app
state is what ultimately drives the UI and how the user interacts with your app.
This has always been true, but SwiftUI embraces this reality with specific APIs
and a unidirectional data flow. UIKit-based apps or features were &lt;em&gt;also&lt;/em&gt; driven
by state, but this state would often live in the UI layer and was much more
hidden as a result.&lt;/p&gt;
&lt;div class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This is actually true for frontend/web apps as well, as React works in a
very similar way as SwiftUI and also uses a unidirectional data flow. In
fact, I suspect SwiftUI drew some inspiration from React in terms of API
design, but we probably won't ever get that confirmed :)&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;SwiftUI improves the data flow in applications, but there is much less
discussion on best practices for defining what that data looks like. This is too
bad, because thoughtful data and state modeling has cascading effects in API
design and testability of code. Suboptimal definitions of state in the the model
layer means that view models, view controllers, views, etc. all also suffer from
that suboptimal design.&lt;/p&gt;
&lt;p&gt;No matter the app, state plays a critical role (even if that state largely comes
from the server) and I wanted to write down best practices and common pitfalls
I've seen when writing state models.&lt;/p&gt;
&lt;h2 id="prefer-value-types"&gt;Prefer value types&lt;/h2&gt;
&lt;p&gt;State is often passed down or otherwise accessed by different parts of the app.
In some cases you might want to mutate it or operate on a separate copy of that
state, without immediately affecting every other part of the app that relies on
that same state. Using value types provides the necessary guard rails to make
sure those changes don't unintentionally also impact other parts of the app,
while still making it possible, to also globally mutate state.&lt;/p&gt;
&lt;p&gt;Making the main state type a struct or an enum is a good start but not always
sufficient: &lt;em&gt;all&lt;/em&gt; state in a tree is ideally using value types. This avoids
misusing a state type to be more than just the basic values that represent the
current condition of the app.&lt;/p&gt;
&lt;p&gt;It also inherently means that almost all state should be a collection of
"primitives" like &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Float&lt;/code&gt;/&lt;code&gt;Double&lt;/code&gt;, and &lt;code&gt;Bool&lt;/code&gt; that are used
either directly or wrapped in other types (e.g. &lt;code&gt;CGRect&lt;/code&gt;,
&lt;code&gt;NumberFormatter.Style&lt;/code&gt;, a custom type, etc.).&lt;/p&gt;
&lt;p&gt;Basically any other type is out. No classes, closures, actors, or other more
complex types as they typically aren't good candidates to represent state.
Unfortunately, &lt;a href="https://forums.swift.org/t/status-of-se-0283-tuples-conform-to-equatable-comparable-and-hashable/46942/3"&gt;tuples&lt;/a&gt; are also not an option for now, but using a named
struct is usually worth that hassle.&lt;/p&gt;
&lt;p&gt;I've found that a good way to validate this is by making all state types conform
to &lt;code&gt;Equatable&lt;/code&gt; (or possibly &lt;code&gt;Sendable&lt;/code&gt;). Types whose implementation of those
&lt;code&gt;Equatable&lt;/code&gt; don't require a custom implementation of &lt;code&gt;==&lt;/code&gt; are good to go, and if
the compiler complains about non-conformance there's some critical thinking to
do.&lt;/p&gt;
&lt;h2 id="avoid-singletons"&gt;Avoid singletons&lt;/h2&gt;
&lt;p&gt;If you needed more reasons to dislike singletons, here is another one: it makes
data modeling more difficult. Take this &lt;code&gt;UserManager&lt;/code&gt; example:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Ignoring all other singleton-related issues, the problem here is that since
&lt;code&gt;UserManager&lt;/code&gt; can be accessed even if the user isn't logged in, it gets hard to
model the &lt;code&gt;user&lt;/code&gt; property:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make it optional and you're dealing with optionality everywhere, which is also
  not ideal (more on that below)&lt;/li&gt;
&lt;li&gt;Make it "empty" (e.g. &lt;code&gt;var user = User(id: UUID(), name: "")&lt;/code&gt;, and it doesn't
  properly reflect the state the app could actually be in since a &lt;code&gt;user&lt;/code&gt; isn't
  required for the app to function&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's better to architect this in a different way such that you can guarantee the
&lt;code&gt;user&lt;/code&gt; and its properties are valid (i.e. non-nil and not just some default
value) when asking for it.&lt;/p&gt;
&lt;h2 id="separate-dtos-from-domain-models"&gt;Separate DTOs from domain models&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Data Transfer Objects&lt;/strong&gt; are the objects you get back directly from, typically,
an API. DTOs are directly bound to the data the API sends, which could have all
kinds of problems: missing or unused fields, wrong data types, optionality in
some cases but not in others, unexpected or inconsistent formats, different
versions, etc.&lt;/p&gt;
&lt;p&gt;It's tempting to use these DTOs directly, especially when using mechanisms like
Codable to create them easily and to cover some of those flaws. However, it
might also be tempting to &lt;em&gt;not&lt;/em&gt; optimize the domain model for your app because
Codable isn't always the easiest to work it. This could handicap your data
models as they now have to follow the API design and all its flaws everywhere in
your app.&lt;/p&gt;
&lt;p&gt;Admittedly, it is a bit annoying to write the data model "twice", but the work
often pays off and the decoupling is worth it. Perhaps AI or other tools can
help with this as well.&lt;/p&gt;
&lt;h2 id="avoid-state-impossibilities"&gt;Avoid state impossibilities&lt;/h2&gt;
&lt;p&gt;Imagine a &lt;code&gt;User&lt;/code&gt; object that captures the user's email and its verification
status:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;emailVerified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;The &lt;code&gt;verified&lt;/code&gt; status is required regardless of whether there is an email
address. But the combination &lt;code&gt;email = nil&lt;/code&gt; and &lt;code&gt;emailVerified = true&lt;/code&gt; means
nothing, so the state is modeling an impossible scenario. It's better to model
this as a tuple or a struct:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="c1"&gt;// or&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;

    &lt;span class="c1"&gt;// with optional conveniences:&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;emailAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;emailVerified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;verified&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Now either both fields are set, or neither are and the app doesn't have to
handle that "impossible" state.&lt;/p&gt;
&lt;h2 id="minimize-redundant-state"&gt;Minimize redundant state&lt;/h2&gt;
&lt;p&gt;Try to use the minimum amount of state possible to represent a scenario. For
example, you might start out with a simple flag to choose whether an account has
been selected, but the app's requirements later change to also capture &lt;em&gt;which&lt;/em&gt;
account:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;AccountSelection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;hasSelectedAccount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;selectedAccount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Not only does this lead to another impossible state (&lt;code&gt;hasSelectedAccount =
true&lt;/code&gt; and &lt;code&gt;selectedAccount = nil&lt;/code&gt;), but &lt;code&gt;hasSelectedAccount&lt;/code&gt; can also be
inferred from &lt;code&gt;selectedAccount&lt;/code&gt;: if it's &lt;code&gt;nil&lt;/code&gt;, no account has been selected.&lt;/p&gt;
&lt;p&gt;Don't be lazy and let these redundancies build up in your app because the core
business logic and UI layers (and tests!) will have to deal with this
unnecessary complexity.&lt;/p&gt;
&lt;h2 id="reduce-optionals-when-possible"&gt;Reduce optionals when possible&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The power of optionals is not using them&lt;/p&gt;
&lt;p&gt;- Someone, at some point&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In languages that have an &lt;code&gt;Optional&lt;/code&gt; type, use them as little as possible. An
optional immediately forks a type into two possible values (&lt;code&gt;some&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt;),
which incurs an additional code branch to test.&lt;/p&gt;
&lt;p&gt;Note that "as little as possible" doesn't mean "never", so here are a few common
pitfalls I've encountered when an optional is the best tool available:&lt;/p&gt;
&lt;h3 id="handle-optionality-as-early-as-possible"&gt;Handle optionality as early as possible&lt;/h3&gt;
&lt;p&gt;If you're calling an API that returns an optional value that you expect to
always be there anyway, try to eliminate it as early as possible instead of
letting that optional permeate your entire tech stack. Define your model with
the value as a non-optional and assert on its existence in your model (or even
networking) layer.&lt;/p&gt;
&lt;p&gt;In line with the DTO use above, that could look like this:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;

    &lt;span class="kd"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserDTO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// UserDTO might not have a user&amp;#39;s name for some reason&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;ParsingError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missingField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;This eliminates the need to test for what happens at every other layer (UI,
business logic, etc.) if that value were to be nil.&lt;/p&gt;
&lt;h3 id="optional-vs-empty-arrays"&gt;Optional vs. empty arrays&lt;/h3&gt;
&lt;p&gt;A pattern I see a lot, unfortunately also in Apple's code, is an array defined
like this:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;myStrings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;The optional here makes the code more difficult to deal with, since now you have
to account for the &lt;code&gt;nil&lt;/code&gt; case everywhere, in addition to the array being empty.&lt;/p&gt;
&lt;p&gt;Hot take: in the vast majority of cases, code doesn't handle a &lt;code&gt;nil&lt;/code&gt; array
differently from an empty array (search your codebase for &lt;code&gt;?? []&lt;/code&gt; and you'll see
what I mean), so you might as well model it like that too.&lt;/p&gt;
&lt;p&gt;In the situations where that difference &lt;em&gt;is&lt;/em&gt; meaningful, making the property
optional is fine:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;friends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;In this case, &lt;code&gt;nil&lt;/code&gt; could mean we don't know who the user's friends are, and
an empty array could mean the user has no friends ☹️&lt;/p&gt;
&lt;h3 id="consider-enums-instead-of-optionals"&gt;Consider enums instead of optionals&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Result&lt;/code&gt; type was introduced in Swift's standard library to eliminate the
awkward situation where both the value and the error are optional types yet one
of them will always be set. (Another example of state impossibility: what does
it mean if both the value &lt;em&gt;and&lt;/em&gt; the error are set? Or neither?)&lt;/p&gt;
&lt;p&gt;I've often seen this awkwardness in state as well, and similar to &lt;code&gt;Result&lt;/code&gt;, the
solution is often to model that state as an enum with associated values. As a
simplified example, instead of a bunch of optionals in an onboarding flow where
data is collected step by step, that state could be modeled as:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;OnboardingState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;askForName&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;askForBirthday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;askForEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;birthday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;askForPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;birthday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;h2 id="closing"&gt;Closing&lt;/h2&gt;
&lt;p&gt;Not all of the techniques above will be useful all the time, but I've often
found myself adjusting the data model when something was annoying to deal with
in layers that consumed it, and over time realized the importance of getting the
details there right to make the rest of the application easier to deal with too.&lt;/p&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Architecture"/><category term="Testing"/></entry><entry><title>Shifting the testing culture: Code coverage</title><link href="https://scottberrevoets.com/2024/11/20/shifting-the-testing-culture-code-coverage/" rel="alternate"/><published>2024-11-20T00:00:00-08:00</published><updated>2024-11-20T00:00:00-08:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2024-11-20:/2024/11/20/shifting-the-testing-culture-code-coverage/</id><summary type="html">&lt;div class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This is the third post in a series on shifting our testing culture.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://scottberrevoets.com/2024/11/18/shifting-the-testing-culture-motivation/"&gt;Motivation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://scottberrevoets.com/2024/11/19/shifting-the-testing-culture-infrastructure/"&gt;Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code coverage&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p&gt;Most people will say that code coverage is a measure of how well-tested your
code is. I prefer to say it's a measure of how &lt;em&gt;untested&lt;/em&gt; a codebase is. Take
this example …&lt;/p&gt;</summary><content type="html">&lt;div class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This is the third post in a series on shifting our testing culture.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://scottberrevoets.com/2024/11/18/shifting-the-testing-culture-motivation/"&gt;Motivation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://scottberrevoets.com/2024/11/19/shifting-the-testing-culture-infrastructure/"&gt;Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code coverage&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p&gt;Most people will say that code coverage is a measure of how well-tested your
code is. I prefer to say it's a measure of how &lt;em&gt;untested&lt;/em&gt; a codebase is. Take
this example:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;myFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;zero&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;one&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;other&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;testMyunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;This yields 66.7% code coverage, but although the test passes, it doesn't
validate any business logic because the test has no assertion. The example is a
bit derived, but in a more realistic scenario where tools report you have 66.7%
code coverage, you don't actually know if that code is actively tested or just
happens to be executed while running tests. What you do know is that the other
33.3% is &lt;em&gt;definitely not&lt;/em&gt; tested.&lt;/p&gt;
&lt;h2 id="llvm-cov"&gt;&lt;code&gt;llvm-cov&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Pedantries aside, code coverage is still important even if the coverage level is
already high and there isn't much untested code. That's because code coverage
isn't just a number, it also comes with a detailed &lt;strong&gt;source overlay&lt;/strong&gt;: the
source code with highlights that indicate which lines, functions, and code
branches were invoked while running tests, and how often:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Source overlay as generated by llvm-cov" src="/images/source-overlay.png"&gt;&lt;/p&gt;
&lt;p&gt;It's clear that although this method has a test, the &lt;code&gt;input == 0&lt;/code&gt; branch wasn't
executed while running them. The &lt;code&gt;return "one"&lt;/code&gt; and &lt;code&gt;return "other"&lt;/code&gt; lines are
executed once, and the rest of the method is executed twice. Getting full
coverage of this function requires adding a test that calls it with &lt;code&gt;input ==
0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;On iOS, Xcode has built-in support for reporting code coverage. It presumably
uses &lt;a href="https://llvm.org/docs/CommandGuide/llvm-cov.html"&gt;llvm-cov&lt;/a&gt; for user friendliness, but invoking &lt;code&gt;llvm-cov&lt;/code&gt; directly gives a
ton of extra options, including exporting in different formats like JSON and
HTML.&lt;/p&gt;
&lt;p&gt;Since &lt;code&gt;llvm-cov&lt;/code&gt; operates on a test bundle and we have a test bundle for each of
our modules, we can get detailed information about a module's code coverage:
which file and which lines within a file, how many total lines are testable and
how many are covered, etc.&lt;/p&gt;
&lt;p&gt;In the past we used the various output formats to integrate with &lt;a href="https://codecov.io"&gt;codecov&lt;/a&gt;, but
eventually moved to an in-house web portal where this data is stored and easily
accessible. We've also built tooling integration so developers can run a simple
command on their own machine to get &lt;code&gt;llvm-cov&lt;/code&gt;'s output generated for their
modules, though viewing it in Xcode is often the better choice for local
development. The only part that's missing is seeing the source overlays in
GitHub so you can immediately tell which code changes require more tests.&lt;/p&gt;
&lt;h2 id="tracking-coverage"&gt;Tracking coverage&lt;/h2&gt;
&lt;p&gt;Every time a module's source code or tests change, we capture the new coverage
metrics in a database to keep track of a full history of each module's code
coverage. We aggregate the data by team and for the codebase as a whole, and
display it on the web portal. This gives anyone immediate access to answer
common questions about their team's testing progress.&lt;/p&gt;
&lt;p&gt;Developers are often interested in more than just the raw code coverage numbers
- they also want to understand what code isn't tested so they can add more
coverage. To make this easier, the web portal also displays the source overlays
for all source files in a module for easy viewing without having to open Xcode
for it.&lt;/p&gt;
&lt;h2 id="minimum-coverage"&gt;Minimum coverage&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;When a measure becomes a target, it ceases to be a good measure&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;- Goodhart's law&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Many companies enforce a minimum coverage level all code has to hit. Although it
sounds enticing, it could also turn into exactly what Goodhart's law warns
about. Developers will likely work around it if they feel they need to, or write
low-quality tests just to hit the minimum. It's also not clear what number to
pick and what to base that on. &lt;/p&gt;
&lt;p&gt;So instead we took a slightly different approach: each module gets to set &lt;em&gt;its
own minimum coverage percentage&lt;/em&gt; in the &lt;code&gt;BUILD&lt;/code&gt; file, and that percentage is
then enforced by our CI system. It's a good balance between establishing a
minimum while giving developers enough control to not make it feel overbearing.&lt;/p&gt;
&lt;p&gt;Developers are free to set that minimum to any percentage they want through a
pull request, but lowering it too drastically requires a good justification and
comes with social friction. Increases are often celebrated as a job well done.&lt;/p&gt;
&lt;p&gt;We also bump the minimum for a given module automatically if it's too low
compared to its actual coverage. A module whose minimum coverage is set to 10%
but actually has 50% coverage, would get its minimum increased to 45%. This
makes it harder to accidentally drop coverage.&lt;/p&gt;
&lt;p&gt;Other ideas we've played around with but haven't implemented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;not allowing decreases in minimum coverage below a certain threshold&lt;/li&gt;
&lt;li&gt;enabling a global minimum but setting it very low (e.g. 20%), in hopes of
  having to write &lt;em&gt;some&lt;/em&gt; tests will lead to writing more&lt;/li&gt;
&lt;li&gt;higher minimum coverage for code that uses highly testable architectural
  components (as opposed to legacy code that's harder to test)&lt;/li&gt;
&lt;li&gt;tech lead approval for dropping a module's minimum coverage&lt;/li&gt;
&lt;li&gt;requiring adding/updating tests on PRs (changing code without needing to
  change tests generally means the code is not tested)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="raising-the-bar"&gt;Raising the bar&lt;/h2&gt;
&lt;p&gt;Having a long history of coverage data available has been a big factor in
raising the quality bar. During biannual planning, teams get a clear overview of
their average coverage and can quickly see which of their modules have the most
room for improvement, and attach a measurable goal to it.&lt;/p&gt;
&lt;p&gt;We look at overall trends and average coverage levels per module type, and set
goals for them for as guidance for other teams. Nothing big, just a couple
percent points per quarter, but it makes testing a talking point. When we
introduce a new tool, e.g. snapshot tests, we can see how that affects the trend
(spoiler alert: our developers strongly prefer snapshot tests) and whether to
invest more time/effort.&lt;/p&gt;
&lt;p&gt;Developers ask for more or better tests during code review. There has been a
noticeable uptick in code changes that just introduce a bunch more tests for
existing code. More refactors start out with ensuring code coverage is solid
or by closing any testing gaps. If an incident happens, the fix is asked to
include a test, and the post-mortem documents the code coverage of the code
that was at fault, with an action item to increase coverage if it's too low.&lt;/p&gt;
&lt;p&gt;Just like a more testable architecture and better tooling, having insightful
code coverage data played a big role in the gradual shift of our iOS testing
culture. We started with near 0% coverage, and all the improvements over the
years have gotten us to an overall code coverage of 60% and climbing.&lt;/p&gt;
&lt;p&gt;To me it's one of the most fascinating leaps in maturity I've seen in my time at
Lyft with a ton of learnings along the way.&lt;/p&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Testing"/><category term="Developer Experience"/><category term="Engineering Culture"/></entry><entry><title>Shifting the testing culture: Infrastructure</title><link href="https://scottberrevoets.com/2024/11/19/shifting-the-testing-culture-infrastructure/" rel="alternate"/><published>2024-11-19T00:00:00-08:00</published><updated>2024-11-19T00:00:00-08:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2024-11-19:/2024/11/19/shifting-the-testing-culture-infrastructure/</id><summary type="html">&lt;div class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This is the second post in a series on shifting our testing culture.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://scottberrevoets.com/2024/11/18/shifting-the-testing-culture-motivation/"&gt;Motivation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Infrastructure&lt;/li&gt;
&lt;li&gt;&lt;a href="https://scottberrevoets.com/2024/11/20/shifting-the-testing-culture-code-coverage/"&gt;Code coverage&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p&gt;This second article goes a bit deeper into the testing infrastructure we built
over the years to optimize the ergonomics and developer experience as much as
possible.&lt;/p&gt;
&lt;p&gt;Our initial test suites only …&lt;/p&gt;</summary><content type="html">&lt;div class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This is the second post in a series on shifting our testing culture.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://scottberrevoets.com/2024/11/18/shifting-the-testing-culture-motivation/"&gt;Motivation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Infrastructure&lt;/li&gt;
&lt;li&gt;&lt;a href="https://scottberrevoets.com/2024/11/20/shifting-the-testing-culture-code-coverage/"&gt;Code coverage&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p&gt;This second article goes a bit deeper into the testing infrastructure we built
over the years to optimize the ergonomics and developer experience as much as
possible.&lt;/p&gt;
&lt;p&gt;Our initial test suites only had basic tests for some foundational code in
shared modules. Initially these were written in &lt;a href="https://github.com/Quick/Quick"&gt;Quick and Nimble&lt;/a&gt;, but we moved
to the default &lt;code&gt;XCTest&lt;/code&gt; to reduce the learning curve. When we started
writing UI tests, we immediately used &lt;code&gt;XCUITest&lt;/code&gt; and still use that today.&lt;/p&gt;
&lt;p&gt;(&lt;code&gt;swift-testing&lt;/code&gt; looks very nice and we're exploring how we can start using this
in our own codebase.)&lt;/p&gt;
&lt;h2 id="modules"&gt;Modules&lt;/h2&gt;
&lt;p&gt;When we modularized the codebase (which I wrote a little bit about &lt;a href="https://scottberrevoets.com/2021/10/14/ios-architecture-at-lyft/#modules"&gt;before&lt;/a&gt;), we
did so specifically with testing in mind. All modules got their own test bundle
to test that module's code.&lt;/p&gt;
&lt;p&gt;Later on, we also specialized each module to the type of code it contained. A
module with primarily UI code in it is a &lt;code&gt;UI&lt;/code&gt; module, and a module with
core business logic is a &lt;code&gt;logic&lt;/code&gt; module. This approach has three (testing)
benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It separates business logic from UI logic at a higher level&lt;/li&gt;
&lt;li&gt;You can now establish the level of testing that's expected per module type
   (e.g. higher for &lt;code&gt;logic&lt;/code&gt; modules than for &lt;code&gt;UI&lt;/code&gt; modules)&lt;/li&gt;
&lt;li&gt;The module type defines what type of tests should be written for that module
   (e.g. snapshot tests for &lt;code&gt;UI&lt;/code&gt; modules, unit tests for &lt;code&gt;logic&lt;/code&gt; modules)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Since each module is owned and maintained by a specific team, that team is now
fully in control over the tests they want to write for their own features. For
example, a UI-heavy feature is more likely to have snapshot tests than unit
tests. Complicated flows might have both or use UI tests for integration-like
tests.&lt;/p&gt;
&lt;h2 id="avoiding-flakiness"&gt;Avoiding flakiness&lt;/h2&gt;
&lt;p&gt;Test flakiness, tests that sometimes pass and sometimes fail, became more common
as we grew the number tests. This is a problem for multiple reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;people lose confidence in the system if tests fail when they shouldn't&lt;/li&gt;
&lt;li&gt;it slows engineers down&lt;/li&gt;
&lt;li&gt;it's disruptive when tests that have been around for months suddenly start
  failing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The source of flakiness was generally an infrastructure problem, a test problem,
or a code problem. Infrastructure problems were usually not actionable by
product engineers and was something related to CI, Xcode, the test runner, the
simulator, or the intersection between any of these.&lt;/p&gt;
&lt;p&gt;If the code was flaky that may have actually been a bug (yay tests!). Often this
meant some globally mutable state was involved, and was different than what the
tests expected for some reason.&lt;/p&gt;
&lt;p&gt;If the test itself was flaky, it wasn't always obvious why and how that could be
avoided. The code is hard to debug because the issue doesn't always reproduce
(sometimes only on CI) and if there didn't seem to be any bugs people would
understandably give up on that test if tracking the issue down took too long.&lt;/p&gt;
&lt;p&gt;On the infrastructure side we've made many improvements over the years to reduce
flakiness to a minimum:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retry a failing test to reduce the impact of infrastructure failures&lt;/li&gt;
&lt;li&gt;disable a test that failed multiple times in a row and report it to the
  maintainer of the module that has the failing test&lt;/li&gt;
&lt;li&gt;increase the timeouts for test suites to run to avoid timing out slow-running,
  but passing, tests&lt;/li&gt;
&lt;li&gt;keep track of common errors out of our control (e.g. bugs in Xcode) and
  explicitly ignore them, try again, or work around them&lt;/li&gt;
&lt;li&gt;ensure tests always ran in the same environment: iOS version, device simulator,
  locale, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Infrastructure flakiness still happens on rare occasion, but the sytems are much
more robust and forgiving of intermittent issues than when we first started.&lt;/p&gt;
&lt;h2 id="making-simple-things-simple"&gt;Making simple things simple&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Simple things should be simple, complex things should be possible.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;- Alan Kay&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Further architecture improvements led us to standardize on a Unidirectional
Data Flow using RxSwift. These patterns are very testable, but required quite a
bit of boilerplate and significant knowledge of best testing practices within
those systems.&lt;/p&gt;
&lt;p&gt;With a more RxSwift-centric architecture, more asynchronousness code patterns
emerged as well. Initially we used &lt;code&gt;XCTestExpectation&lt;/code&gt;s to deal with that since
that's what Apple recommended, but regularly having to increase the timeout on
these expectations felt bad and we figured there had to be better ways.&lt;/p&gt;
&lt;p&gt;We first adopted RxBlocking, but later realized RxTest was where the real money
was for us. Having two extra libraries just for testing felt heavy-handed at the
time, but have become invaluable for us.&lt;/p&gt;
&lt;p&gt;Because the new standard for features was that they were all written using
in-house architectural components without too much deviation, we could also
standardize the test-writing part through a thin glaze of syntax sugar. Testing
a single action's outcome in a reducer now looks like this:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;testAcknowledgingError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="c1"&gt;// easy error state setup with mock data&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acknowledgeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// acknowledge the error&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// error has been acknowledged&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;With many conveniences like these in place, many tests are easy to read and take
just a few lines of code. I'm optimistic that once we adopt &lt;code&gt;swift-testing&lt;/code&gt;'s
parameterized testing we can probably reduce this even more.&lt;/p&gt;
&lt;h2 id="mocking"&gt;Mocking&lt;/h2&gt;
&lt;p&gt;Our &lt;a href="https://scottberrevoets.com/2021/10/14/ios-architecture-at-lyft/#dependency-injection"&gt;dependency injection system&lt;/a&gt; enables mocking with protocols which carries a
bit more boilerplate than I'd prefer but isn't too bad overall. We've also built
"automock" tooling: a pre-compile tool that generates mock classes for all
protocols it found in a module. That looks something like this:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// handwritten by a developer:&lt;/span&gt;

&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="nc"&gt;MyAPI&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getDataFromServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// json&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;postDataToServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// autogenerated by automock:&lt;/span&gt;

&lt;span class="n"&gt;open&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyAPIMock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyAPI&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;getDataFromServerReturn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;getDataFromServerCalls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="n"&gt;postDataToServerParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getDataFromServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;getDataFromServerCalls&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;getDataFromServerReturn&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;postDataToServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;postDataToServerParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;&lt;code&gt;MyAPIMock&lt;/code&gt; can be used in tests to verify that the right methods are called at
the right time, and with the right parameters.&lt;/p&gt;
&lt;p&gt;Automock supports more complicated use cases like closures, and more complex
parameter and return types as well. This saves developer time writing mock
classes and promotes consistency in naming and mocking usage.&lt;/p&gt;
&lt;p&gt;It even works for model data, but it's less used and I'm more interested in a
macro approach that looks something like this:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Mock&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// @Mock expands to:&lt;/span&gt;

&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Generating mock data is now as simple as &lt;code&gt;User.mock()&lt;/code&gt; (or even &lt;code&gt;.mock()&lt;/code&gt; if the
type can be inferred) while still having the ability to set parameters manually
if needed.&lt;/p&gt;
&lt;h2 id="other-improvements"&gt;Other improvements&lt;/h2&gt;
&lt;p&gt;We've made a plethora of other improvements as well: sharding to parallelize
tests, only running test bundles for changed modules, a &lt;code&gt;TestKit&lt;/code&gt; module to
remove common &lt;code&gt;setUp()&lt;/code&gt; boilerplate, and a bunch of small things I'm probably
forgetting right now.&lt;/p&gt;
&lt;p&gt;But the most meaningful and impactful improvements have been related to how we
handle code coverage. So much so that I thought it deserved a whole separate
post.&lt;/p&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Testing"/><category term="Architecture"/><category term="Developer Experience"/></entry><entry><title>Migration strategies in large codebases</title><link href="https://scottberrevoets.com/2022/11/15/migration-strategies-in-large-codebases/" rel="alternate"/><published>2022-11-15T00:00:00-08:00</published><updated>2022-11-15T00:00:00-08:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2022-11-15:/2022/11/15/migration-strategies-in-large-codebases/</id><summary type="html">&lt;p&gt;Code migrations are a fact of life for large codebases. New technologies pop up,
platforms see improvements, and programming languages get new features you might
want to adopt. Not performing these migrations and keeping up with the times is
simply not an option in most cases. While some migrations are …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Code migrations are a fact of life for large codebases. New technologies pop up,
platforms see improvements, and programming languages get new features you might
want to adopt. Not performing these migrations and keeping up with the times is
simply not an option in most cases. While some migrations are easy 1 for 1
replacements that can/should be scripted, that’s not true for the more
“semantic” migrations—the new code accomplishes the same but in an entirely
different manner.&lt;/p&gt;
&lt;p&gt;We’ve done a number of these semantic migrations in Lyft’s mobile codebases and
while we’ve learned something from every one of them (and applied the learnings
the next time), they only get harder as time goes on.&lt;/p&gt;
&lt;p&gt;What makes migrations so difficult? This generally comes down to how development
works in large codebases: there is no 1 mobile team, but rather a lot of
vertical teams (e.g. payments, new user experience, etc.) that all build their
own features, using shared foundational code and architectural patterns. The
coordination of dozens of teams on the progress of a migration is what makes
this difficult:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Codebases tend to grow ad infinitum, so there is simply more code to migrate
  each time and there are more “weird” cases that make automating a migration
  difficult.&lt;/li&gt;
&lt;li&gt;Getting (and keeping) buy-in and commitment. Teams all have their own plans
  and priorities, and migrating code from pattern A to pattern B is often not
  high on the priority list. If it is, that could change before the migration is
  completed.&lt;/li&gt;
&lt;li&gt;Senior engineers on those teams tend to have good judgment on the importance
  of reducing tech debt and can weigh it against other priorities coming from
  product managers and designers. Junior engineers that get pulled in 4
  different directions are less likely to push back against their direct
  teammates, so tech debt tickets are moved to an ever-expanding Jira backlog.&lt;/li&gt;
&lt;li&gt;Teams are not static. You might inherit code written years ago that still
  works and is in use, but you didn’t even know it existed until last week. And
  now it needs to be migrated? Or it’s that one feature that is somehow mission
  critical but no one knows how to even get in the state to show it, let alone
  test new code in it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So how do we get these migrations done anyway? There is no silver bullet, but we
have started using a number of strategies to make these migrations less
painful—and almost all of them have to do with improving the communication and
expectations everyone involved has.&lt;/p&gt;
&lt;h2 id="migration-tracker"&gt;Migration tracker&lt;/h2&gt;
&lt;p&gt;The biggest leap forward for us was the rollout of an in-house web dashboard
that shows progress on all ongoing migrations in our iOS and Android codebases.
It shows all the important information anyone could want to know: the start
date, (projected) completion date, links to documentation and support channels,
relative priority (more on that below), etc.&lt;/p&gt;
&lt;p&gt;But the real win is automatic progress tracking. Every migration is defined as
the presence of a certain pattern in the codebase we want to get rid of. A cron
job records all instances of that pattern on the main branch once a day, so we
know exactly how much code has been migrated and how much is left.&lt;/p&gt;
&lt;p&gt;Furthermore, our codebase is heavily modularized, and through CODEOWNERS every
module is required to define what team owns it. This data is shared with the
Migration Tracker, so it can give a detailed breakdown by module &lt;em&gt;and by team&lt;/em&gt;
of how much unmigrated code is left. This is extremely helpful in understanding
which teams might need extra time, help, or nudging in getting everything done.&lt;/p&gt;
&lt;h2 id="timeline"&gt;Timeline&lt;/h2&gt;
&lt;p&gt;The “deadline” of a migration needs to be carefully chosen. Not giving people
enough time makes them not do the work at all, giving them too much time means
people will procrastinate and needlessly extend the migration time. If it looks
like teams are not going to make the deadline (as projected by the Migration
Tracker), we ask them when they &lt;em&gt;can&lt;/em&gt; get it done. We usually accept whatever
answer the team gives, but hold them accountable to that since they themselves
picked that timeframe.&lt;/p&gt;
&lt;h2 id="priorities"&gt;Priorities&lt;/h2&gt;
&lt;p&gt;Teams that are far behind in code modernization might have to migrate their code
in multiple ways. We give each migration a priority (P0/P1/P2/P3) to give teams
a sense of where to focus their tech debt energy first.&lt;/p&gt;
&lt;p&gt;A P0 migration is a “must-do-or-else” (externally mandated changes, tight
deadlines, extremely high impact on overall trajectory of the codebase or
product, etc.), a P1 is high impact but not as urgent, P2 is a nice to have, and
P3 is almost more of an FYI.&lt;/p&gt;
&lt;p&gt;This helps everyone understand where they should focus their energy, instead of
seeing a potentially long list of items that are all seemingly equally important
migrations.&lt;/p&gt;
&lt;h2 id="incremental-value"&gt;Incremental value&lt;/h2&gt;
&lt;p&gt;Where possible (which is most cases), we try to bring incremental value as a
migration is being performed. For example, if we set out to replace all uses of
class A with class B because class B performs better, the places where class B
is used should see that performance increase before the rest of the code has
been migrated. This has a few benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It rewards teams that have done their part of the overall migration&lt;/li&gt;
&lt;li&gt;It’s easier to convince people doing their part has value since they
  themselves immediately see that value&lt;/li&gt;
&lt;li&gt;It de-risks long-running migrations—if it gets to 95% completion we realize
  95% of the benefits and not 0%&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="only-invest-in-the-new"&gt;Only invest in the new&lt;/h2&gt;
&lt;p&gt;The initial value (like performance improvements in the example above) teams get
from migrating their own code might not be enough to convince them to do it. But
as more and more improvements are made to “class B” (e.g. API ergonomics,
integrated analytics, compatibility with other systems) that “class A” doesn’t
have, the value proposition for the migration changes with time. We no longer
update A, which means there's some amount of bit rot and it becomes more
cumbersome to use.&lt;/p&gt;
&lt;p&gt;In extreme cases we could even remove features of class A that would necessitate
a migration, but we haven’t done that so far and would like to avoid it to avoid
losing goodwill with teams.&lt;/p&gt;
&lt;h2 id="develop-great-partnerships"&gt;Develop great partnerships&lt;/h2&gt;
&lt;p&gt;Getting adoption of a new pattern or tool is difficult even if it doesn’t
involve a new migration, because there is no precedent. Early adopters will hit
all the rough edges and have often incomplete documentation and no sample code.
The developer experience isn’t great (yet), so we make ourselves very available
to their needs. We take their feedback seriously and address it promptly, sit
down with them to help solve their issues, and generally spend a lot of time
with them working out the kinks. We do this 1) to build more confidence that
what we built is production-ready and 2) to recognize/reward people that want to
try new things by reducing as much of the growing pains as possible.&lt;/p&gt;
&lt;p&gt;Of course, good developer experience is important for all teams and not just
early adopters, but piloting teams can help us get the ball rolling with other
teams as well. Positive reviews from users of Great New Thing is different than
hearing it from the developers of Great New Thing.&lt;/p&gt;
&lt;h2 id="avoid-new-uses"&gt;Avoid new uses&lt;/h2&gt;
&lt;p&gt;To avoid creating a moving target, as soon as we start a new migration we lock
down the uses of that pattern to only the current ones and no longer accept new
uses of this pattern. Depending on the situation we have a few different ways of
accomplishing this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We add a linter rule or sometimes entirely new type of (custom) linter and
  introduce exceptions to all existing use cases&lt;/li&gt;
&lt;li&gt;We rename a class or method by prepending the current name with &lt;code&gt;DEPRECATED&lt;/code&gt;.
  This isn’t particularly elegant, but it alerts the author and PR reviewers
  that new legacy code is being introduced, which usually has them dig deeper
  and figure out what to do instead.&lt;/li&gt;
&lt;li&gt;If an entire module is being deprecated, we don’t easily let new modules
  depend on it. Exceptions are always possible, but engineers have to talk to us
  first so we can guide them in the right direction.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These measures are just to generate awareness that a pattern is outdated, it
doesn’t always communicate why it’s being deprecated and what’s replacing it.
But if people come to us asking for an exception we can explain it or link to
docs, an opportunity we wouldn’t have had if we didn’t do any of the things
above.&lt;/p&gt;
&lt;h2 id="support-support-support"&gt;Support, support, support&lt;/h2&gt;
&lt;p&gt;And that brings me to the most critical and time-consuming piece: support. This
means helping people with questions, plugging gaps new patterns have compared to
the old ones, checking in with people on progress, celebrating the completion of
major (or all!) migrations with everyone that helped contribute, assisting teams
that are having difficulty prioritizing the work, and anything else that gets
the work done.&lt;/p&gt;
&lt;p&gt;Migrations are a very specific type of codebase maintenance due to the large
footprint they often have in a lot of the codebase. It’s an all-hands on deck
situation and requires proper coordination and communication, sometimes for
years on end. It’s hard to get them right and we’re still fine-tuning our
strategies but applying the learnings I described here have made us become
better at them. I can only hope one day refactoring tools become smart enough to
automate the work for us.&lt;/p&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Technical Debt"/><category term="Engineering Culture"/></entry><entry><title>Third-party libraries are no party at all</title><link href="https://scottberrevoets.com/2022/07/15/third-party-libraries-are-no-party-at-all/" rel="alternate"/><published>2022-07-15T00:00:00-07:00</published><updated>2022-07-15T00:00:00-07:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2022-07-15:/2022/07/15/third-party-libraries-are-no-party-at-all/</id><summary type="html">&lt;p&gt;&lt;em&gt;What better way to end the week than with a hot take?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In my 8 years at Lyft, product managers or engineers have often wanted to add
third-party libraries to one of our apps. Sometimes it’s necessary to integrate
with a specific vendor (like PayPal), sometimes it’s to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;What better way to end the week than with a hot take?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In my 8 years at Lyft, product managers or engineers have often wanted to add
third-party libraries to one of our apps. Sometimes it’s necessary to integrate
with a specific vendor (like PayPal), sometimes it’s to avoid having to build
something complicated, and sometimes it’s simply to not reinvent the wheel.&lt;/p&gt;
&lt;p&gt;While these are generally reasonable considerations, the risks and associated
costs of using a third-party library are often overlooked or misunderstood. In
some cases the risk is worth it, but to be able to determine that you first need
to be able to define that risk accurately. To make that risk assessment more
transparent and consistent, we defined a process of things we look at to
determine how much risk we're incurring by integrating it and shipping it in one
or more production apps.&lt;/p&gt;
&lt;h2 id="risks"&gt;Risks&lt;/h2&gt;
&lt;p&gt;Most larger organizations, including ours, have some form of code review as part
of their development practices. For those teams, adding a third-party library is
equivalent to adding a bunch of unreviewed code written by someone who doesn't
work on the team, subverting the standards upheld during code review and
shipping code of unknown quality. This introduces risk in how the app runs,
long-term development of the app, and, for larger teams, overall business risk.&lt;/p&gt;
&lt;h3 id="runtime-risks"&gt;Runtime risks&lt;/h3&gt;
&lt;p&gt;Library code generally has the same level of access to system resources as
general app code, but they don't necessarily apply the best practices the
team put in place for managing these resources. This means they have access
to the disk, network, memory, CPU, etc. without any restrictions or
limitations, so they can (over)write files to disk, be memory or CPU hogs
with unoptimized code, cause dead locks or main thread delays, download (and
upload!) tons of data, etc. Worse, they can cause crashes or even &lt;a href="https://www.theverge.com/2020/5/7/21250689/facebook-sdk-bug-ios-app-crash-apple-spotify-venmo-tiktok-tinder"&gt;crash
loops&lt;/a&gt;. &lt;a href="https://github.com/facebook/facebook-ios-sdk/issues/1427"&gt;Twice&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Many of these situations aren't discovered until the app is already available to
customers, at which point fixing it requires creating a new build and going
through the review process which is often time intensive and costly. The risk
can be somewhat mitigated by invoking the library behind a feature flag, but
that isn't a silver bullet either (see below).&lt;/p&gt;
&lt;h3 id="development-risks"&gt;Development risks&lt;/h3&gt;
&lt;p&gt;To quote a coworker: "every line of code is a liability", and this is even more
true for code you didn't write yourself. Libraries could be slow in adopting new
technologies or APIs holding the codebase back, or too fast causing a deployment
target that's too high. When Apple and Google introduce new OS versions each
year, they often require developers update their code based on changes in their
SDKs, and library developers have to follow suit. This requires coordinated
efforts, alignment in priorities, and the ability to get the work done in a
timely manner.&lt;/p&gt;
&lt;p&gt;As the mobile platforms are ever-changing this becomes a continuous, ongoing
risk, compounded by the problem that teams and organizations aren't static
either. When a library that was integrated by a team that no longer exists needs
to be updated, it takes a long time to figure out who should do so. It has
proven extremely rare and extremely difficult to remove a library once it's
there, so we treat it as a long-term maintenance cost.&lt;/p&gt;
&lt;h3 id="business-risks"&gt;Business risks&lt;/h3&gt;
&lt;p&gt;As I mentioned above, modern OSes make no distinction between app code and
library code, so in addition to system resources they also have access to user
information. As app developers we're responsible for using that information
properly, and any libraries are part of that responsibility.&lt;/p&gt;
&lt;p&gt;If the user grants location access to the Lyft app, any third-party library
automatically gets access too. They could then upload that data to their own
servers, competitors' servers, or who knows where else. This is even more
problematic when a library needs a &lt;em&gt;new&lt;/em&gt; permission we didn't already have.&lt;/p&gt;
&lt;p&gt;Similarly, a system is as secure as its weakest link but if you include
unreviewed, unknown code you have no idea how secure it really is. Your
well-designed secure coding practices could all be undone by one misbehaving
library. The same goes for any policies Apple and Google put in place like "you
are not allowed to fingerprint the user".&lt;/p&gt;
&lt;h2 id="mitigating-the-risk"&gt;Mitigating the risk&lt;/h2&gt;
&lt;p&gt;When evaluating a library for production usage, we ask a few questions to
understand the need for the library in the first place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can we build this functionality in-house?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In some cases we were able to simply copy/paste the parts of a library we really
needed. In more complex scenarios, where a library talked to a custom backend we
reverse-engineered that API and built a mini-SDK ourselves (again, only the
parts we needed). This is the preferred option 90% of the time, but isn't always
feasible when integrating with very specific vendors or requirements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many customers benefit from this library?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In one scenario, we were considering adding a very risky library (according to
the criteria below) intended for a tiny subset of users while still exposing all
of our users to the library. We ran the risk of something going wrong for all
our customers in all our markets for a small group of customers we thought would
benefit from it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What transitive dependencies does this library have?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We'll want to evaluate the criteria below for all dependencies of the library as
well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What are the exit criteria?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If integration is successful, is there a path to moving it in-house? If it isn't
successful, is there a path to removal?&lt;/p&gt;
&lt;h2 id="evaluation-criteria"&gt;Evaluation criteria&lt;/h2&gt;
&lt;p&gt;If at this point the team still wants to integrate the library, we ask them to
“score” the library according to a standard set of criteria. The list below is
not comprehensive but should give a good indication of the things we look at.&lt;/p&gt;
&lt;h3 id="blocking-criteria"&gt;Blocking criteria&lt;/h3&gt;
&lt;p&gt;These criteria will prevent us from including the library altogether, either
technically or by company policy, and need to be resolved before we can move
forward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Too high deployment target/target SDKs&lt;/strong&gt;. We support major OSes going back
  at least 4 years so third-party libraries have to support at least that many
  too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improper/missing LICENSE&lt;/strong&gt;. We bundle license files with the apps to make
  sure we can legally use the code and attribute it to the license holders.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No conflicting transitive dependencies&lt;/strong&gt;. A library can't have a transitive
  dependency we already include but at a different version.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does not display its own UI&lt;/strong&gt;. We put great care in making our products look
  as uniform as possible, custom UIs are a detriment to that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does not use private APIs&lt;/strong&gt;. We are not willing to risk app rejection over
  the use of private APIs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="major-concerns"&gt;Major concerns&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Closed source&lt;/strong&gt;. Access to the source means we can pick and choose which
  parts of a library we want to include and how to bundle that source with the
  rest of the app. A closed source, binary distribution is much more difficult
  for us to integrate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Builds with warnings&lt;/strong&gt;. We have "warnings as errors" enabled, and a library
  with build warnings is a decent indication of the overall quality of the
  library.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poor documentation&lt;/strong&gt;. We look for high quality inline docstrings, external
  "how to use" documentation, and meaningful change logs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binary size impact&lt;/strong&gt;. How big is this library? Some libraries provide a lot
  of functionality of which we only need a fraction. Especially without source
  access it's often an all-or-nothing situation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External network traffic&lt;/strong&gt;. A library that communicates with upstream
  servers/endpoints we have no control over could take the entire app down when
  the server is down, bad data is sent back, etc. This also has the same privacy
  implications I mentioned above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical support&lt;/strong&gt;. When things don't work as they should we need to be
  able to report/escalate issues and get them fixed in a reasonable amount of
  time. Open source projects are often run by volunteers and usually can't
  commit to timelines, but at least we could make changes ourselves. This is
  impossible without source access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inability to disable&lt;/strong&gt;. While most libraries specifically require us to
  initialize it, some are more "proactive" in their instantiation and will
  perform work themselves without us ever calling into it. This means when the
  library causes issues we have no option to turn it off through feature flags
  or other mechanisms.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We assign point values to all these (and a few others) criteria and ask
engineers to tally those up for the library they want to include. While low
scores aren't hard-rejected by default, we often ask for more justification to
move forward.&lt;/p&gt;
&lt;h2 id="final-notes"&gt;Final notes&lt;/h2&gt;
&lt;p&gt;While this process may seem very strict and the potential risk hypothetical in
many cases, we have actual, real examples of every scenario I described in this
blog post. Having the evaluations written down and publicly available also helps
in conveying relative risk to people unfamiliar with how mobile platforms works
and demonstrating we're not arbitrarily evaluating the risks.&lt;/p&gt;
&lt;p&gt;Also, I don't want to claim every third-party library is inherently bad. We
actually use quite a few at Lyft: RxSwift and RxJava, Bugsnag's SDK, Google
Maps, Tensorflow, and a few smaller ones for very specific use cases. But all of
these are either well-vetted, or we've decided the risk is worth the benefit
while actually having a clear idea of what those risks and benefits really are.&lt;/p&gt;
&lt;p&gt;Lastly, as a developer pro-tip: always create your own abstractions on top of
the library's APIs and never call their APIs directly. This makes it much easier
to swap (or remove) underlying libraries in the future, again mitigating some
risk associated with long-term development.&lt;/p&gt;
&lt;div class="admonition update"&gt;
&lt;p class="admonition-title"&gt;Update (February 2026)&lt;/p&gt;
&lt;p&gt;The industry has changed a ton over the years but this topic is still very
much relevant. Many vendors have decided to write their own SDKs which are
difficult to avoid, and I don't think writing an in-house SDK for a
third-party vendor really caught on. The growth of Apple's platforms
(hardware capabilities, OSes, Swift and Xcode versions, etc.) and associated
complexity make some of these SDKs also inherently more complex and buggier.&lt;/p&gt;
&lt;p&gt;Fortunately, LLMs have shifted the buy vs build decision more in the
direction of build, and Apple's investment in modern/capable APIs and
privacy manifests made it easier to opt for them than introducing another
dependency. More tooling is available to monitor dependencies and keeping
them up to date.&lt;/p&gt;
&lt;p&gt;I don't think companies have become more thoughtful about the risks they
take on with third-party dependencies, but platform, tooling, and workflow
improvements at least make it a little bit easier to choose which vendor (be
it first, second, or third-party) suits them best.&lt;/p&gt;
&lt;/div&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Technology Adoption"/><category term="Technical Debt"/></entry><entry><title>iOS Architecture at Lyft</title><link href="https://scottberrevoets.com/2021/10/14/ios-architecture-at-lyft/" rel="alternate"/><published>2021-10-14T00:00:00-07:00</published><updated>2021-10-14T00:00:00-07:00</updated><author><name>Scott Berrevoets</name></author><id>tag:scottberrevoets.com,2021-10-14:/2021/10/14/ios-architecture-at-lyft/</id><summary type="html">&lt;p&gt;June 30, 2014 was my first day at Lyft as the first iOS hire on the ~3 person
team. The app was written in Objective-C, and the architecture was a 5000-line
nested switch statement.&lt;/p&gt;
&lt;p&gt;Since then, the team has grown to about 70 people and the codebase to 1.5M …&lt;/p&gt;</summary><content type="html">&lt;p&gt;June 30, 2014 was my first day at Lyft as the first iOS hire on the ~3 person
team. The app was written in Objective-C, and the architecture was a 5000-line
nested switch statement.&lt;/p&gt;
&lt;p&gt;Since then, the team has grown to about 70 people and the codebase to 1.5M lines
of code. This required some major changes to how we architect our code, and
since it had been a while since we've given an update like this, now seems as
good a time as any.&lt;/p&gt;
&lt;h2 id="requirements"&gt;Requirements&lt;/h2&gt;
&lt;p&gt;The effort to overhaul and modernize the architecture began around mid-2017.
We started to reach the limits of the patterns we established in the 2015
rewrite of the app, and it was clear the codebase and the team would continue to
grow and probably more rapidly than it had in the past.&lt;/p&gt;
&lt;p&gt;The primary problems that the lack of a more mature architecture presented and
that we wanted to solve were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolation&lt;/strong&gt;: Features were heavily intertwined, which made it difficult to
  safely make changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testability&lt;/strong&gt;: We were still mostly following MVC with view controllers being
  the main source of business logic which made it difficult to test that logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State management&lt;/strong&gt;: The navigation in most of our app relied on local +
  server state, which grew with the number of features. How and when state
  changed then became too difficult to manage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There was not going to be one solution that would solve all of this inherently,
but over the course of a few years we developed a number of processes and
technical solutions to reduce these problems.&lt;/p&gt;
&lt;h2 id="modules"&gt;Modules&lt;/h2&gt;
&lt;p&gt;First, to provide better feature separation, we introduced modules. Every
feature had its own module, with its own test suite, that could be developed in
isolation from other modules. This forced us to think more about public APIs and
hiding implementation details behind them. Compile times improved, and it
required much less collaboration with other teams to make changes.&lt;/p&gt;
&lt;p&gt;We also introduced an ownership model that ensured each module has at least one
team that's responsible for that module's tech debt, documentation, etc.&lt;/p&gt;
&lt;h3 id="module-types"&gt;Module types&lt;/h3&gt;
&lt;p&gt;After fully modularizing the app and having 700 modules worth of code, we took
this a step further and introduced a number of &lt;em&gt;module types&lt;/em&gt; that each module
would follow.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;UI&lt;/code&gt; modules only contain UI elements (views, view controllers)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Flow&lt;/code&gt; modules contain &lt;a href="#flows"&gt;routing infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Service&lt;/code&gt; modules contain code to interact with endpoints related to the
  feature's functionality&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Logic&lt;/code&gt; modules contain pure business logic, data transformations, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Breaking modules down this way enabled us to implement dependency validators: we
can validate that certain modules can't depend on others. For example, a logic
module can't depend on a &lt;code&gt;UI&lt;/code&gt; module, and a &lt;code&gt;Service&lt;/code&gt; module can't import UIKit.&lt;/p&gt;
&lt;p&gt;This module structure also prevents complicated circular dependencies,
e.g. a &lt;code&gt;Coupons&lt;/code&gt; module depending on &lt;code&gt;Payments&lt;/code&gt; and vice versa. Instead, the
&lt;code&gt;Payments&lt;/code&gt; module can now import &lt;code&gt;CouponsUI&lt;/code&gt; without needing to import the full
&lt;code&gt;Coupons&lt;/code&gt; feature. It's led to micromodules in some areas, but we've generally
been able to provide good tooling to make this easier to deal with.&lt;/p&gt;
&lt;p&gt;All in all we now have almost 2000 modules total for all Lyft apps.&lt;/p&gt;
&lt;h2 id="dependency-injection"&gt;Dependency Injection&lt;/h2&gt;
&lt;p&gt;Module types solved many of our dependency tree problems at the module level,
but we also needed something more scalable than singletons at the code level.&lt;/p&gt;
&lt;p&gt;For that we've built a lightweight dependency injection framework which we
detailed in a &lt;a href="https://www.youtube.com/watch?v=dA9rGQRwHGs"&gt;SLUG talk&lt;/a&gt;. It resembles a service locator pattern, with a
basic dictionary mapping protocols to instantiations:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;getNetworkCommunicator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NetworkCommunicating&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NetworkCommunicating&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;NetworkCommunicator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;The implementation of &lt;code&gt;bind()&lt;/code&gt; doesn't immediately return &lt;code&gt;NetworkCommunicator&lt;/code&gt;,
but requires the object be mocked if we're in a testing environment:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;productionInstantiators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;ObjectIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;mockedInstantiators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;ObjectIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instantiator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ObjectIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;NSClassFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;XCTestCase&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;productionInstantiators&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;instantiator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mockedInstantiators&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;In tests, the mock is required or the test will crash:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NetworkingTests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;XCTestCase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;communicator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MockNetworkCommunicator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;testNetworkCommunications&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NetworkCommunicating&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;communicator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;This brings two benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It forces developers to mock objects in tests, avoiding production side
   effects like making network requests&lt;/li&gt;
&lt;li&gt;It provided a gradual adoption path rather than updating the entire app at
   once through some more advanced system&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Although this framework has some of the same problems as other &lt;a href="https://en.wikipedia.org/wiki/Service_locator_pattern"&gt;Service
Locator&lt;/a&gt; implementations, it works well enough for us and the limitations are
generally acceptable.&lt;/p&gt;
&lt;h2 id="flows"&gt;Flows&lt;/h2&gt;
&lt;p&gt;Flows, inspired by Square's &lt;a href="https://square.github.io/workflow/"&gt;Workflow&lt;/a&gt;, are the backbone of all Lyft apps.
Flows define the navigation rules around a number of related screens the user
can navigate to. The term &lt;code&gt;flow&lt;/code&gt; was already common in everyday communications
("after finishing the in-ride flow we present the user with the payments flow")
so this terminology mapped nicely to familiar terminology.&lt;/p&gt;
&lt;p&gt;Flows rely on state-driven routers that can either show a screen, or route to
other routers that driven by different state. This makes them easy to compose,
which promoted the goal of feature isolation.&lt;/p&gt;
&lt;p&gt;At the core of flows lies the &lt;code&gt;Routable&lt;/code&gt; protocol:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="nc"&gt;Routable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;viewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;UIViewController&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;It just has to be able to produce a view controller. The (simplified) router
part of a flow is implemented like this:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Routable&lt;/span&gt;&lt;span class="p"&gt;?)]&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;addRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;routable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Routable&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routable&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Routable&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;first&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;In other words: it takes a bunch of rules where if the condition is true
(accepting the flow's state as input) it provides a &lt;code&gt;Routable&lt;/code&gt;. Each flow defines
its own possible routes and matches those to a &lt;code&gt;Routable&lt;/code&gt;:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;OnboardingState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;verificationCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OnboardingFlow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;router&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OnboardingState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OnboardingState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phoneNumber&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;EnterPhoneNumberViewController&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verificationCode&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;VerifyPhoneViewController&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;EnterEmailViewController&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="c1"&gt;// If all login details are provided, return  `nil` to indicate this flow has&lt;/span&gt;
        &lt;span class="c1"&gt;// no (other) Routable to provide and should be exited&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;currentRoutable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Routable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;We're then composing flows by adding &lt;code&gt;Routable&lt;/code&gt; conformance to each flow and
have it provide a view controller, adding its current &lt;code&gt;Routable&lt;/code&gt;s view
controller as a child:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Routable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;rootViewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;UIViewController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;parent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;UIViewController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentRoutable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;viewController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Now a flow can also route to another flow by adding an entry to its router:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;needsOnboarding&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;OnboardingFlow&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;This pattern could let you build entire trees of flows:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Simplified flow diagram" src="/images/flows.png"&gt;&lt;/p&gt;
&lt;p&gt;When we first conceptualized flows we imagined having a tree of about 20 flows
total; today we have more than 80. Flows have become the "unit of development"
of our apps: developers no longer need to care about the full application or a
single module, but can build an ad-hoc app with just the flow they're working
on.&lt;/p&gt;
&lt;h2 id="plugins"&gt;Plugins&lt;/h2&gt;
&lt;p&gt;Although flows simplify state management and navigation, the logic of the
individual screens within a flow could still be very intertwined. To mitigate
that problem, we've introduced plugins. Plugins allow for attaching
functionality to a flow without the flow even knowing that the plugin exists.&lt;/p&gt;
&lt;p&gt;For example, to add more screens to the OnboardingFlow from above, we can expose
a method on it that would call into its router:&lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;OnboardingFlow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;addRoutingPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;routable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Routable&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
        &lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OnboardingState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addRoute&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routable&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Since this method is public, any plugin that imports it can add a new screen. The
flow doesn't know anything about this plugin, so the entire dependency tree is
inverted with plugins. Instead of a flow depending on all the functionalities of
all of its plugins, it provides a simple interface that lets plugins extend this
functionality in isolation by having them depend on the flow.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Simplified plugin setup" src="/images/plugins.png"&gt;&lt;/p&gt;
&lt;p&gt;Since all Lyft apps operate on a tree of flows, the overall dependency graph
changes from a tree shape to a "bubble" shape:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Bubble dependency graph" src="/images/graph.png"&gt;&lt;/p&gt;
&lt;p&gt;This setup provides feature isolation at the compiler level which makes it much
harder to accidentally intertwine features. Each plugin also has its own feature
flag, making it very easy to disable a feature if necessary.&lt;/p&gt;
&lt;p&gt;In addition to routing plugins, we also provide interfaces to add additional
views to any view controller, deep link plugins to deep link to any arbitrary
part of the app, list plugins to build lists with custom content, and a few
others very unique to Lyft's use cases.&lt;/p&gt;
&lt;h2 id="unidirectional-data-flow"&gt;Unidirectional Data Flow&lt;/h2&gt;
&lt;p&gt;More recently we introduced a redux-like unidirectional data flow (UDF) for
screens and views within flows. Flows were optimized for state management within
a collection of screens, the UDF brings the same benefits we saw there to
individual screens.&lt;/p&gt;
&lt;p&gt;A typical redux implementation has state flowing into the UI and actions that
modify state coming out of the UI. Influenced by &lt;a href="https://github.com/pointfreeco/swift-composable-architecture"&gt;The Composable
Architecture&lt;/a&gt;, our implementation of redux actions also includes executing
side effects to interact with the environment (network, disk, notifications,
etc.).&lt;/p&gt;
&lt;h2 id="declarative-ui"&gt;Declarative UI&lt;/h2&gt;
&lt;p&gt;In 2018, we began building out our &lt;a href="https://design.lyft.com/building-a-design-system-library-3a1f0d09088f"&gt;Design System&lt;/a&gt;. At the time, it was a
layer on top of UIKit, often with a slightly modernized API, that would provide
UI elements with common defaults like fonts, colors, icons, dimensions, etc.&lt;/p&gt;
&lt;p&gt;When Apple introduced SwiftUI in mid-2019, it required a deployment target of
iOS 13. At the time, we still supported iOS 10 and even today we still support
iOS 12 so we still can't use it.&lt;/p&gt;
&lt;p&gt;However, we did write an internal library called &lt;code&gt;DeclarativeUI&lt;/code&gt;, which provides
the same declarative APIs that SwiftUI brings but leveraging the Design System
we had already built. Even better, we've built &lt;code&gt;binding&lt;/code&gt; conveniences into both
DeclarativeUI and our UDF &lt;code&gt;Store&lt;/code&gt; types to make them work together seamlessly: &lt;/p&gt;
&lt;details class="code-block" open&gt;&lt;summary&gt;&lt;span class="code-block-arrow" aria-hidden="true"&gt;&lt;/span&gt;&lt;span&gt;Swift&lt;/span&gt;&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;DeclarativeUI&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Unidirectional&lt;/span&gt;

&lt;span class="kr"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuestionView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeclarativeUI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;viewStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QuestionState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="kd"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QuestionState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;viewStore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeclarativeUI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;VStackView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;three&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;HeaderView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textStyle&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;titleF1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textAlignment&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;center&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accessibility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postScreenChanged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;VStackView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="n"&gt;TwoChoiceButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touchUpInside&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;choiseSelected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;viewStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usesButtonToIncrementQuestion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;NextQuestionButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;h2 id="putting-it-all-together"&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;All these technologies combined make for a completely different developer
experience now than five years ago. Doing the right thing is easy, doing the
wrong thing is difficult. Features are isolated from each other, and even
feature components are separated from each other in different modules.&lt;/p&gt;
&lt;p&gt;Testing was never easier: unit tests for modules with pure business logic,
snapshot tests for UI modules, and for integration tests it takes little effort
to sping up a standalone app with just the flow you're interested in.&lt;/p&gt;
&lt;p&gt;State is easy to track with debug conveniences built into the architectures,
building UI is more enjoyable than it was with plain UIKit, and adding a feature
from 1 app into another is often just a matter of attaching the plugin to a
second flow without detangling it from all other features on that screen.&lt;/p&gt;
&lt;p&gt;It's amazing to look back at where the codebase started some 6 years ago, and
where it is now. Who knows where it will be in another 6 years!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: If you're interested in hearing more, I also talked about many of these
technologies on the &lt;a href="https://podcasts.apple.com/us/podcast/mobile-architecture-pt-1-with-scott-berrevoets/id1453587931?i=1000512549072"&gt;Lyft Mobile Podcast&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;</content><category term="Architecture &amp; Infrastructure"/><category term="Architecture"/><category term="Platform Engineering"/></entry></feed>