3

Preparing Flexport’s Data Models for the Future

 1 year ago
source link: https://flexport.engineering/preparing-flexports-data-models-for-the-future-5c221d1dc3fa
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Preparing Flexport’s Data Models for the Future

For the past 3 months, I have had the opportunity to intern at Flexport on the Shipment Platform team. This team is focused on building cross functional products and tackling the engineering challenges of a growing organization. I spent much of my internship on a long overdue undertaking: maintaining and investing in one of Flexport’s core data models. My key takeaway from this experience is that the scope and simplicity of core services should be constantly evaluated and invested in.

Flexport’s Data Models

As a freight forwarding company, Flexport’s data primarily corresponds with shipments moving from point A to point B across the world, typically by ocean. Our first pass at modeling this consisted of four nodes on a map: the origin, departure port, arrival port, and destination. With this pattern, we found it difficult to represent shipments in motion, and to indicate whether these shipments were on ships, trucks, trains, or planes. So edges, or legs as we call them, were created to connect these nodes. As Flexport and its number of service offerings grew, the complexity of shipments grew as well. A single shipment could be split up among different paths through the world and have intermediate stops. Four nodes could not adequately represent this, so the data model evolved to have arbitrary structure, now known as the Graph.

Internally, the Graph is the backbone of Flexport engineering. It’s one of the most heavily used data models at Flexport because it is at the heart of every shipment and allows a constant, up-to-date view of relevant information across the organization. This is important because Flexport’s product teams are split by service offering, and most shipments use multiple services to reach their destinations. A simple shipment from China to the US might involve a trucking leg from the factory to the port, an ocean leg across the Pacific, a rail leg across the United States, and finally another trucking leg from the railyard to the distribution center. Without the Graph, the coordination required to properly move any shipment would be much more difficult.

Externally, the Graph is the client’s window into their shipments. A Graph is created the moment a quote is requested and is continuously updated as it progresses into a completed shipment. The Graph represents the latest plan for the shipment before departure and presents a client with real time information while en route.

1*Hr85Ytr3I58Ozp81lB0noA.png

This is a visual representation of the Graph that corresponds to a shipment. Each node represents a place, and each edge represents unique movement between two nodes.

During my onboarding, I learned that, as a result of scaling, interacting with the Graph had become overly complicated and difficult. Some of the primary issues include:

  1. The Graph has a large surface area. As the concept has grown with the company, a lot of domain or team specific data and functions have been added. As a result, it is difficult to understand as an engineer and unwieldy to navigate.
  2. The Graph has a lot of unconventional patterns. Reading and writing to the Graph is now a difficult task and requires niche knowledge that has to be handed down or reverse engineered. In addition, the Graph contains a significant amount of denormalized data and duplicated functions.
  3. The Graph is overly coupled with complex front-end components. Besides reading and writing directly to models themselves, the main write interface surrounding the Graph was designed to accept input from the front end components that relied on it. Over time, that interface also became the preferred way to write to the Graph.
  4. The Graph has very little documentation despite the issues mentioned. This has hindered product velocity and lengthened the engineering onboarding process.

Encapsulation and Boundaries

As part of a larger company objective to build a solid engineering foundation, one of the most critical pieces was bringing the Graph up to speed. It was far too complex and had too many dependencies in its initial iteration. Just before I joined Flexport, my team had decided the first step to fixing this was to encapsulate the Graph through a series of boundaries.

Our first priority was to define interfaces for all other parts of the code base to interact with the Graph. Otherwise, making changes to the Graph would require a significant amount of effort to migrate the rest of the codebase. With boundaries, we would be able to make changes to the Graph while managing only the implementation details of the interface.

0*rZoqZtDdSRJxqanj

It made sense to create three separate boundaries for reading, writing, and triggering side effects. The first of these that we worked on was the write boundary. Other than reducing migration efforts, it was important that we design an interface that is also easy for engineers to use and understand. If done successfully, this would be a high leverage project that would increase velocity and remove any confusion and appearance of magic that surrounded writing to the Graph. The company planned on scaling heavily, and the problem of developer ergonomics needed to be solved.

The interface had to have a maintainable scope and an easy-to-use design in order to succeed. We compiled a list of code snippets and files that wrote to the Graph, and I cataloged what pieces of the data model they added to, deleted, or modified. This allowed us to get an accurate sense of the scope that our new interface needed. Having little experience working at Flexport or with the Graph, it was difficult for me to envision the actual challenges I would be face. This research process opened my eyes to the the various issues that existed and kickstarted the brainstorming process. For completeness sake, we also instrumented any writes to the Graph that occurred directly on the data models. To get a holistic sense for the design, we interviewed each stakeholder team about upcoming use cases they would be implementing, difficulties they had felt when trying to use the Graph, and their ideal abstractions. I got a view into how varied each team’s experience with the Graph could be by sitting through these interviews.

Design

After we brainstormed designs, we settled on a builder interface that was based around our old write interface, known as GraphForm. The idea of an interface around an interface confused me at first. I wasn’t sure why we weren’t just making changes to GraphForm directly. It seemed like we had invested a lot into GraphForm’s functionality, and I later discovered that, as a result of being coupled to the front end, it received a set of nested hashes as input which was difficult for an engineer to reason through. To make it more comprehensible, it required a major change in front end logic. That task would have as much complexity as modifying Graph itself, so we decided that the path forward was to use GraphForm as a core to GraphFormBuilder. The functionality and scope reduction that GraphForm provided was simply too valuable and discarding it would force us to reinvent the wheel. In addition, our exploration of the codebase had presented us with examples of teams creating builder interfaces around GraphForm. From this discovery, it made sense to design our interface in a similar manner. Ultimately, GraphForm proved to be an implementation detail that made the most sense given the context. Our boundaries will allow us to change this in the future if the need arises.

Our second critical decision was to make the interface fluent. I was hesitant at first because I had never worked on a fluent interface before. This also was the first time in my career that I had an extended discussion on the merits of “update” vs “for” vs “at” and verb choice in the context of code. However, this seemingly pointless conversation was meant to solve the issue of documentation and code complexity.

An example of the fluent interface and its simplicity

With a fluent interface, the documentation is baked right into the the library and the code should be as readable as ordinary English. It took away a lot of the mysticism that deciphering a set of Graph writes took, even for someone without the proper context on Graph. Transformations involving the builder and functions wrapping a builder would all have semantic meaning without a need to look under the hood. This is especially beneficial when onboarding new engineers because it reduces the amount of knowledge that needs to be handed down.

Implementation

After we finished planning, the only task that remained was implementation.Rather than just building out GraphFormBuilder, our team migrated all the use cases to make sure we didn’t block other teams. I took a ‘just in time’ approach to the implementation of GraphFormBuilder, exposing only as much surface area as was needed to migrate a single use case. This approach kept velocity high and ensured that there was no overinvestment. At the end of migration, it gave me the freedom to either implement missing functions or continue with further work. The migrations themselves ranged in difficulty. Complex writes to the Graph required a lot of team context and time to understand the functionality in order to achieve feature parity. That resulted in a lot of cross team collaboration and provided me the opportunity to touch multiple codebases and learn about the internals of other services. One example was migrating the utility that kept our Graph’s air legs in sync with real time departure and arrival data. There were multiple types of events that could flow in and change the Graph, sometimes in ways that were not always apparent from reading the implementation.

The other challenge we faced during migration was supporting brand new work. Through the course of the project, multiple teams came up to us having heard of the new interface and asked whether it could fulfill their use case. As a work in progress, we faced the decision of carrying on with our migration and allowing them to use old patterns, or implementing more of GraphFormBuilder to fully support their feature. In most cases, we opted for the latter in order to set new patterns. Many might rely on prior art for inspiration, and we believed having more examples in the codebase, and more engineers familiar with the pattern would help adoption in the long run.

0*cG-bFZ8oPbNNlpln

A piece of functionality before it was migrated to GraphFormBuilder

0*Go3tTzCZVHjg2gOM

The same functionality after it was migrated to GraphFormBuilder

Lessons

As of writing this blog post, we have migrated over two thirds of the company to GraphFormBuilder and helped two teams use GraphFormBuilder on their own. We have received lots of enthusiasm over how simple the interface is to use. After the completion of this project, we will be one step closer to making the necessary changes to the Graph.

This process taught me a lot about time management, project planning, designing, and navigating a large codebase. Some of my key takeaways are:

  • Measure twice and cut once. You do this by investing in research and valuing design over iteration. When defining a new way for an entire organization to interact with core services, it is difficult or expensive to have more than one chance to get it right. Therefore, knowing the most common use cases, pain points, and needs of end users can simplify the design process. A great way to identify these details is through interviewing stakeholders.
  • Account for lead time in collaboration. Although the migrations were done by our team to minimize disruption, a lot of domain specific knowledge was required to be successful. I learned that baking in lead time and starting conversations asynchronously can reduce the amount of time trying to understand the intention of code and otherwise being blocked.
  • Know where to draw lines and hold your ground in preventing scope creep. For example, there were times when we wanted to expand the scope of the GraphFormBuilder interface to include useful helpers. However we realized that just like the Graph grew in scope over time, the interface would grow unnecessarily if we began to work on more domain specific functions. We limited ourselves to the building blocks and opted to allow teams to create their own helpers.

I’d like to extend a big thank you to Dounan Shi and Will Pitler as my mentors on this work, as well as the rest of the Shipment Platform team for making my internship experience wonderful!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK