As a Staff Software Engineer on the Mobile Team at SoFi, my day-to-day consists of building and maintaining the Flutter application that millions of members rely on for their financial lives. Our mobile codebase is a monorepo with more than two million lines of code, over 150 contributors, and weekly releases to the App Store and Google Play. Recently, SoFi undertook a massive migration to unify our entire app under a single, cohesive design system called Pacific — the second most complex migration in our mobile history, rivaled only by our original move from native iOS and Android to Flutter.
What made this migration different was not just its scale, spanning every screen across five business units, but how we executed it: by deeply integrating AI tools into every phase of the process, from scoping and coding to code review and testing
sofi-mobile is a Flutter monorepo that powers the binaries for our flagship Android and iOS apps. We have more than 40,000 tests and release every week to app stores.
This migration started in Q2 2025 when we were just starting to take advantage of LLM (Large Language Models) tools, but over the last few months our AI adoption has increased and is now indispensable in all our workflows. To be clear, this is not a vibe coding, proof of concept or a brainstorming project — this is production code that impacts all SoFi members, hence quality execution is of utmost importance.
A design system is a shared set of components, styles, tokens, patterns, and rules that helps teams build products consistently and faster. At SoFi, Pacific is meant to support accessible, reusable, performance, and visually consistent experiences.
It turns User Interface (UI) work from one-off decisions into a shared product platform through:
A crucial pre-condition for being able to utilize AI is our code base’s solid foundations. For the most part in our code base we have been following best practices including:
Inevitably every large project has blind spots, but typically, code is predictable and follows conventions and this really helped with migrations. As an example: since this is a UI migration, we only have to touch and modify screens, view, widgets and view models and other components such as services, repositories and models are largely unaffected. Also, Flutter’s declarative UI architecture makes for clean separation of concerns.
Regardless of how code is generated by humans or by machines, best practices are vital and LLMs can generate better code if the code base is well architected.
We are migrating code that is being actively worked on by hundreds of developers and used by millions of members. We don’t want to risk introducing bugs and disrupt workflows, so even at the cost of verbosity and slower development, we adopted the following steps.
We came up with a concept of ‘splits’ which loosely maps to a feature. Each BU’s code has been divided into multiple splits, and depending on the BUs code the number of splits varies from 5 splits to 40 splits.
With this division we can allocate developers to work in parallel on various splits and manage their scope, development, testing and deployments.
Let's use a car wash as an analogy. Each car at the car wash represents an independent split.
This is the actual coding step and the meat of the migration. Our aim with the migration is to come out with a better-tested, accessible, instrumented, upgraded and uniform UI with Pacific widgets.
A split goes in and gets all the special treatments and comes ready for rollout. The underlying engines, fuel or in this case, the backend is not changed. We are only changing the visual appearance.
In this step, we do a thorough code review to make sure our established patterns are adhered to. Underlying code can be of differing levels of skill and styles but our migration has to focus on making only the necessary changes.
Additionally, we employ various verification processes both automated with unit, integration and end-to-end tests and Quality Assurance (QA) with manual testers and visual QA with designers.
We are touching code that doesn’t belong to a single team and can have lasting impact. Because of this, we have risk mitigation built in at each phase. We create a clone copy of all visual elements with feature flags for each split.
In our car wash analogy, imagine you get an extra shiny car when you send in an old familiar dingy one.
We roll both of these versions side by side and compare metrics. If there is an issue, rollout is halted and fixes are applied. Otherwise, we keep increasing the rollout of new code and gradually old code along with deprecated widgets is completely and safely deleted.
We need to consolidate information about legacy widgets from code, mapping to new widgets from documentation, keep track of rollouts from a different spreadsheet, keep track of progress for splits and across the whole code base.
We used Cursor for initial scripts generation, MCPs (Model Control Protocols) for google sheets, Claude Code for consolidating information from various docs, acli (Atlassian Command Line Interface) for reading, creating and updating JIRA tasks.
At the end, we have found that one specialized skill like /pacific-money-scope was able to give pointed answers.
During this migration we shifted the underlying test framework from Mockito to Mocktail for reducing code generation time and made tests isolated. Mocktail is a newer mocking library specifically built for the Dart programming language and the Flutter mobile development framework. For this we utilized another skill, /mockito-to-mocktail as a first step into the MR (Merge Request).
We established clear migration patterns and documented them with various examples in /docs/best-practices/pacific-migration
We have simlinks (short for symbolic link, a shortcut or a pointer to another file or folder on your computer) from .claude and .cursor to these best practices and were able to convert big screen files almost mechanically. We created a skill /pacific-migrate which can take in a folder or a file and follow our best practices.
We ensured in our setup that these changes are compilable, tested, linted and formatted before checking in. As mentioned earlier, having all these tools pre-setup and easy ways to verify them locally gave fast feedback to the agents.
On top of human code reviews which are invaluable in the beginning, we started leveraging AI for code reviews. Our first attempt was an internally grown Claude on bedrock implementation called merge-review-bot that does review based on changed diffs. It is good in some respects but leaves a lot to be desired.
Then we leveraged Gitar.ai a 3rd party code review / Continuous Integration fixer tool that was highly customizable and found so many subtle bugs. An excellent feature of Gitar is that it operates on Gitlab hooks and after every code push Gitar updates its review comments and helps guide the MR author to quality code.
Also, we extracted our code review patterns into a skill and codified them. For both bots it helped that we created specific skills and checks.
For test automation both unit and E2E tests we used similar code generation patterns as normal code. We took advantage of Ralph loops to iterate till tests pass. Given that tests can be executed locally, we tasked Claude with continuous iteration until we achieved 80% coverage while adhering to linting and other testing standards.
The SoFi app is complex with a lot of screens and each of them can be in a different state. Each functional unit has their own set of backend services, API interaction models, repositories and business logic. Bringing the app into a state where we can exercise various scenarios is not a trivial task and we have created multiple skills to help with that.
We created specialized skills
MCPs helped bridge the gap of utilizing a tool that doesn’t have a strict API. We used many MCPs including Google Sheets, Google Documents, Datadog, Slack. For authentication and proxy management we leveraged a 3rd party vendor called Cequence and our own MCP wrapper sofi-mcp-cli that wraps across many tool offerings and takes away configuration parts.
Not every tool needs an MCP and in some cases like for JIRA and Gitlab we have found that command line tools worked better than MCPs. A recurring problem with MCPs is authentication but these CLIs even though not strictly specific tools have simple and focussed authentication mechanisms through access tokens. We utilize the acli for JIRA and glab for Gitlab; these command-line interfaces provide robust functionality, enabling us to codify highly efficient interaction mechanisms using custom skills.
Markdown files are an answer to this question: “How do we provide additional information to our agents without creating huge prompts and how do we keep prompts consistent and reusable?“
Since the mobile repository has a big developer base with varying preferences and tool familiarity, we adopted agent agnostic setup. At the top level we created an AI folder
And each of .claude, .cursor simlink to these folders. Additionally we have Agents.md files at different modules and sometimes even at folder levels.
We have several skills that help with various aspects of mobile including testing, debugging, instrumentation, migration
We also created a separate docs/best_practices folder where we listed our repos best practices with examples of good and bad code samples. These best practices are referred throughout our .md files to keep a single source of truth.
At the conclusion of this project, I walked away with a couple learnings specific to AI adoption that I believe will help us to work more efficiently in the future. Crucially, the quality of AI output is directly tied to the guardrails and context management we provide, making these factors paramount for getting deterministic and high-quality results from our agents. Furthermore, while we must observe and quickly adapt to the latest AI tools, our experience showed a preference for utilizing Skills over MCPs, particularly due to challenges with authentication in MCPs, and the need to establish mechanisms to manage the sheer volume of AI output and "slop."
We successfully accomplished a complex, multi-year migration thanks to the many contributors involved. Overall pacific migration across different BUs is largely driven by Engineering tech leads: Mohammad Asgari, Jeff Mascia, Jake Swedenberg, my contributions are narrow and for a single business unit. Special thanks to Phil Rabin, Ashley Branch, and BU Engineers for their guidance and collaboration along the way.