Observations on maintaining shared code internally

Nathanael Beisiegel
IndigoAg.Digital
Published in
7 min readNov 4, 2019
A large tree centered against a crisp Fall sky

Sharing code across projects is a problem that initially seems simple: as simple as a shared folder you extract. This is deceptive, as sharing code can be an enormous organizational challenge as it scales. Consider all the choices that are typically made with growing teams: independent releases, multiple repos, code ownership, software stacks, deployments, dependency management, and build tools. Each of these decisions has a dramatic impact on the way you consume and share code and was likely decided without much consideration for sharing.

Once shared code is pulled out of the single-repo world view, it becomes an inherently social challenge. It requires support, documentation, and upkeep. The way you design your APIs and interfaces around package boundaries requires careful attention to ensure they support all use-cases. These interfaces require careful planning to ensure changes don’t have huge cost when supporting new requirements. Propagating changes requires support and socialization to avoid leaving a long tail of work for others. Even more worrisome, unintentional or ill-considered breaking changes can wreak havoc on cross team projects.

All of these practices have a heavy effect on velocity. When done well, this sharing greatly improves the interaction between teams and abstracts away duplicate efforts. When done poorly, it can frustrate and slow down the entire software development process with confusion and bad abstractions. The stakes are higher: mistakes in sharing code have a much higher cost compared to individual implementations: a size cost of N v.s. 1.

At Indigo we recognize how important it is to invest and maintain a healthy set of packages to support all of our teams. I work with an amazing team to lead and maintain our core UI component projects; and I have previously maintained a suite of packages at a prior startup with a smaller team. I’d like to share some observations on sharing code internally and strategies you can employ at different team sizes.

Designing APIs with shared packages

Sharing code raises the stakes on API design and abstractions. When you get it wrong or make it difficult to extend, you face a much greater cost in fixes and improvements once baked across apps. Prefer copy/paste over a bad abstraction if you don’t have the bandwidth to design shared code properly! Shared code should be shared to share patterns and help with consistency—not to couple similar looking code.

We loosely follow a rule-of-three for most extracted code at Indigo—where we would prefer folks implement solutions or find at least scenarios before sharing. It’s likely that the best designs will fail without realizing the different use-cases. Shared code is a coupling—be careful not to couple the wrong things! You can also recognize the risk in what you are extracting. For example, extracting React components is likely less risky than sharing a set of API utils. The interface of components is extendable and likely easier to change and replace compared to state and async management.

Once you have taken the initiative to extract code, you should consider how you are abstracting. This is not a purely technical decisions—indeed the way you approach this is likely to be more social than technical!

Consider an package with the following desirable traits:

  1. Enforces the right amount of consistency for common use cases via limited choices in abstractions.
  2. Provides “escape hatches” via a maintainable API that exposes deeper composition to allow for uncommon use cases.
  3. Encourages you to do the right thing (accessibility, styling, etc) with the way you use the API.
  4. Does not couple apps to dependencies that do not need to be shared.
  5. Make it difficult or impossible for folks to misuse / rely upon internals in ways you don’t support.
  6. Increases probability of making it easy to support the latest versions of external dependencies like React.

Points 1–5 are all social considerations! A successful implementation for these goals will change based on the team’s way of working, the amount of engineering time the company can invest, and the culture of the organization. Indeed, seeking to enforce consistency and enabling rare use-cases are often in conflict with each other and require a library authors carefully balance the company’s goals of velocity and consistency/quality. For example, if the company cannot invest a lot of time in the shared package and needs feature velocity, a library with convenient escape hatches and fewer assumptions may be desirable over strict consistency and limited API options.

This, in my opinion, is the major value/risk proposition when developing shared code internally. With UI libraries, something off the shelf frequently becomes unsuitable—often due to the lack of escape-hatches and consistency with the organization’s desired experience. Often these problems stem from inflexible consistency, incompatible tooling, painful dependencies, and APIs that do not capture 95% of common use-cases. I constantly recommend that package developers always provide the right escape hatches with patterns like delegation as even the most valiant efforts for consistency end up missing use cases. Guidelines and recommendations can compliment a design system well when you do add these extensible APIs.

Transitioning to better product enablement

Shared code is a big responsibility to take on—why would anyone do that when there are so many UI libraries in the wild? Why are we still reinventing the wheel?

This argument can be blurry when folks discuss UI libraries, as there are so many considerations embedded and it’s easy to move the goalposts. Shared packages do not necessarily mean you are reinventing the wheel, as you may be simply encapsulating styles and defaults on top of native or other library controls.

I think it’s helpful to evaluate this in lens of growing company. Consider an organization that is building MVPs that have met some success. In the early stages of getting a product to market, it’s likely that developers reached for off-the-shelf UI libraries to apply across the apps. In this stage, compromises on the way UI is presented may have been acceptable in the interest of completion. When these products are successful (a great problem to have!) there can be a pivot to quality and addressing edge cases at scale. Both of these put a strain on the compromises of a rigid, external UI library.

This transition is painful. You may be familiar with this as:

  • “What is this CSS override horror show”
  • “Oh no UX wants pagination on a dropdown that we can’t build with the existing component”
  • “Why can’t we build this” — a.k.a. the never-ending, expensive product compromise
  • “We can’t update our UI library as it changes how our navigation will look”
  • “We can’t update our UI library as it would break support with our data library”
  • “Our UI library is no longer supported and we can’t update to the latest React”

I’d like to put an overly formal name on this stage: the cliff of enablement. It’s the breaking point between engineering, UX, and product—where the lack of ownership (and lack of escape hatches) prevent “saying yes” to building nice features. It stops you from building an excellent, unique, and branded product. It’s a symptom of any compromises from any external UI library. Compromises at early stages are hidden and relatively painless. At scale, it’s death by a thousand cuts.

I highly encourage engineers to strategize how to smooth the cliff of enablement as much as possible. Prepare for a transition to more control by saying yes now and preparing to take control later. You can do this by picking a library with an API you are generally happy with (with styling and props). Ensure your tooling allows you to replace UI components incrementally. When transitioning to branded components, you can easily share a component from this third-party library while working to build your own.

Again, gaining more control doesn’t necessary mean rewriting from scratch. When you end up needing this amount of customization, reach for some lower level UI libraries and helpers. I recommend resources such as Downshift and Reach UI, as they do not bake in as many assumptions and assume you will build a proper UI with branding on top of accessible controls (please don’t abandon accessibility for style!). Using dependencies made of lower level primitives strikes a great balance of control while still relying on a community to help you avoid reinventing the wheel with common form components. Adding component delegation via renderProps on top of this allows you to really encapsulate your visual identity while still allowing your feature teams to “say yes to product” for things like adding warnings to dropdowns or adding tooltips to tags within a Multiselect input.

Expect a shift in development and product planning

If you have a team dedicated to maintaining shared code, recognize that the development model will be a lot closer to an open source maintenance model compared to a typical feature development team. Additionally, you will need to think about supporting other teams on upgrades, documentation, and issues—in many cases you may be closer to a DevOps team than a feature team. You will be in an interesting space of consulting, feature development, and developer support.

Ask for feedback often, and integrate often. This will help folks internally avoid too much pain on upgrading and lets you know when changes are not working out. Support helps avoid frustration that could lead others to throw out the baby with the bathwater—where engineers may mistake an implementation failure as a design failure with shared packages.

This requires clear communication and a supportive product team. I want to call out how important it is to have a close partnership with clear workflows on the publishing and implementation roll out of shared code. I’m very thankful for a very collaborative relationship with product and UX here at Indigo—we are able to enable each other to build a world-class product. If you are interested in joining our team, check out the Indigo careers page!

--

--