Introduction
For over a year, the Cash team at Wealthfront has been working towards a consistent transfer experience across the Wealthfront ecosystem. Before beginning this project we had a number of separate transfer flows that each had their distinct user experience. Each transfer flow was built with its own set of UI components, models, and API endpoints. We embarked on a journey to unify these flows, here we will share a few challenges and learnings that we faced so far from a front-end (iOS) engineering perspective.
This fragmented transfer experience was the result of adding new types of accounts and ways of moving money with Wealthfront over the years. Starting with our Automated Investing accounts, we have since added additional transfer needs with the introduction of retirement accounts, Cash Account, Cash Categories, College (529) and Stock investing accounts just to name a few. As we added support for these new transfer types we realized that the existing flows would not easily support the required new functionality. Attempting to add support to these existing flows would involve sizable refactors. Large refactors of money movement code introduced risks that we were unwilling to take on, so we created entirely new flows. This created duplicate logic in many cases that would have to be maintained in isolation. The end result was an inconsistent and at times confusing transfer experience that was on track to become untenable to maintain without some serious infrastructure work.
Queue “One Transfer”.
The ideal transfer infrastructure
One Transfer is an ongoing cross platform project targeted at unifying all existing and future transfer flows into one. The existing legacy transfer flows contain complex front-end logic and state management, making extension and maintenance both costly and error prone. We knew that moving most of this logic to the Wealthfront back-end would allow for simple, flexible, and consistent front-end code across web, android, and iOS.
The high level design/model structure for the project is as follows:
A transfer begins with generation of a TransferIntentView which cleanly maps to “the transfer” being created. The TransferIntentView contains an array of TransferIntentSteps. The path from beginning to end of transfer creation involves the front-end traversing the steps in the TransferIntentView. Each TransferIntentStep contains one or more TransferIntentQuestion that must be satisfied before fetching more information from the back-end in the form of the next step to be completed. Each question is generally satisfied by the selection of a TransferIntentChoice or gathering an answer of the appropriate type depending on each question. This is repeated until the ReviewTransferIntentStep is returned, which is always the last step before submitting the transfer.
First test subject: Cash Categories
We knew a project of this size would not be an easy feat. Migrating the existing functionalities of all the transfer flows at once would be risky. Transferring money reliably is undoubtedly critical; we were building arguably the most important flow. To mitigate this risk we opted to take an incremental approach, beginning with the simplest transfer type: Cash Categories. Migrating Cash Categories was the first chance to put our design to the test. While this was a simple migration, it laid the foundation for all transfer types moving forward. Introducing source and destination selection, amount input, a review screen, and a success screen.
The above screenshots are a great illustration of the One Transfer step structure. Each screenshot is the UI representation of a single TransferIntentStep (except for the last one). From left to right there is the SelectSourceTransferIntentStep, SelectDestinationTransferIntentStep, SetInstructionTransferIntentStep, and the ReviewTransferIntentStep. Aside from the ReviewTransferIntentStep each step contains one question to be answered. The submit response which is represented by the last screenshot, is the terminal state of a TransferIntent which almost always occurs after the ReviewTransferIntentStep.
Balancing simplicity with user experience
Next up was supporting transfers from Cash Accounts to our managed investment accounts. This migration brought forth the added capabilities of recurring transfers, necessitating the choice of a start date and cadence, along with retirement contribution details for retirement accounts. Up until this point, there was only one detail per step (or one question in other words), and one network call per step. Continuing with this pattern, while simple, would result in a transfer experience with too many network calls, creating an unresponsive UI. This presented our first real challenge: How would the front-end manage all these transfer details in a way that would be easy to maintain and provide a pleasant client experience?
The downside of continuing with one question per step was that there would be a network call for every client selection of a transfer detail. In a slow network environment this would be a poor user experience. We ultimately opted to take on some front-end complexity to preserve a responsive and performant UI. This involved bundling all the following transfer details (amount, start date, cadence, retirement contribution details, and portfolio line of credit repayment) into one step. This introduced state management on the front-end because these details would have to be stored until they were sent up to the server.
A reactive approach to maintaining confidence despite complexity
A primary goal for the front-end was ensuring that the information displayed within the UI was always in sync with the transfer instructions stored. It is important to emphasize the potential consequences of submitting a transfer with details inconsistent with what had been shown to the client. This could involve losing trust of clients, performing time consuming trade corrections, and facing the wrath of regulators.
On iOS we thought this was a great use of reactive programming. Our iOS codebase at this time had limited usages of our own reactive UI building framework that was heavily inspired by Apple’s SwiftUI framework. Leveraging this would provide us more safety by establishing strong relationships between the data we were managing and the UI clients were interacting with. This is accomplished through the use of state and binding properties which allow for easy tracking of changes and instant communication with the UI which triggers automatic updates.
While using our reactive programming framework would make updating UI simpler and safer, we still needed something to manage the complexities of actually synchronizing cached details as the client made changes. Some of this complexity comes from a client changing details that have other “child” or dependent details. One example is when a recurring transfer is set up but then changed to one time, the start date and cadence would no longer be relevant. There are many of these cases where details could become stale, irrelevant, or invalid if they were not synchronized following every client interaction. This led to the creation of a component which we call the UnifiedTransfersDetailsDependencyManager or DM. The DM’s sole purpose is to manage the transfer details, ensuring there is no irrelevant or stale state in memory that could lead to errors. This allowed us to keep our UI code as simple as possible.
Adapting to evolving needs
At this stage we had a transfer flow that could handle the essential transfer details. We had support to select a source, destination, transfer instructions, transfer summary, and a confirmation screen. But how would One Transfer handle our future needs for stock investing? Or what about wire transfers? Or withdrawing to an external account?
One Transfer was developed with an approach that provided us with the flexibility we needed for stock transfers, withdrawals, and wire transfers. The design of One Transfer allowed us to confidently add new steps, questions, and choices to support desired functionality. Shipping new features incrementally with emphasis on thorough test coverage allowed us to migrate these flows without any significant issues. Did everything go perfectly? Well there is always a lesson or two to be learned from a project of this size.
Takeaways and conclusion
We learned that building a front-end with the goal of having the same user experience and using the same APIs was not always enough to ensure the same capabilities through the longer term evolution of the One Transfer project. As we began working on wire transfers we quickly realized that each front-end platform handled navigation between steps slightly differently. This prevented us from achieving the desired user experience for wires until each front-end platform had aligned on a single navigation strategy. This was just one example of differences in the front-end implementations creating challenges. It’s important to acknowledge there will be minor differences between the web, iOS, and android implementation. However, we needed to be very intentional about each front-end platform using the same logical implementation, so that a solution that would work for one platform would almost certainly work for the others.
A second lesson we learned working through a long running back-end driven project of this nature is we needed robust mechanisms for gating new functionalities. Any time we add a new feature in the One Transfer flow we need to ensure that old mobile app versions would not be impacted adversely. We accomplished this through the use of a transfer context passed into the TransferIntent generation endpoint. This was used by the back-end to constrain things like the accounts that could be selected or the types of messaging that could be returned. We also leveraged our existing experiment framework, which you can read more about in a separate blog post. Lastly, we provided a way for the back-end to use explicit app version checks to gate functionality. Without these mechanisms we would not have been able to ship the project incrementally, or rollout changes gradually.
One Transfer is still a project in development, the most notable outstanding milestone being the migration of the existing deposit flow. Even though it is still not complete, One Transfer has proven to be far more flexible than the legacy flows, often allowing us to make changes or push fixes with just a back-end commit. While some may measure the success of a migration project by the lines of legacy code removed, it would underscore the immeasurable confidence gained when adding new transfer features. Additionally, the added consistency enables clients to confidently move their funds with Wealthfront.
Disclosures
The information contained in this communication is provided for general informational purposes only, and should not be construed as investment or tax advice. Nothing in this communication should be construed as a solicitation or offer, or recommendation, to buy or sell any security. Any links provided to other server sites are offered as a matter of convenience and are not intended to imply that Wealthfront Advisers or its affiliates endorses, sponsors, promotes and/or is affiliated with the owners of or participants in those sites, or endorses any information contained on those sites, unless expressly stated otherwise. All investing involves risk, including the possible loss of money you invest, and past performance does not guarantee future performance. Please see our Full Disclosure for important details.
Wealthfront offers a free software-based financial advice engine that delivers automated financial planning tools to help users achieve better outcomes. Investment management and advisory services are provided by Wealthfront Advisers LLC, an SEC registered investment adviser, and brokerage related products are provided by Wealthfront Brokerage LLC, a member of FINRA/SIPC. Wealthfront, Wealthfront Advisers and Wealthfront Brokerage are wholly owned subsidiaries of FINRA/SIPC.
© 2023 Wealthfront Corporation. All rights reserved.