At Sparkbox, we view our engagements as partnerships. We don’t want to work for you, we want to work with you. What does that mean? We have lots of conversations. We ask lots of questions. We do a lot of listening. We help you surface problems and determine solutions for your needs. We believe in approaching every client relationship with a consultant mindset.
In 2020, we began a partnership with a company in the autonomous vehicle industry. This company designs and manufactures autonomous warehouse equipment, like forklifts. They approached us to help build user interfaces for the software used to track and manage fleets of these visually-guided vehicles. This software would be used by warehouse workers and floor managers to manage the movements and location of these vehicles.
Throughout this engagement, we were able to employ a consultant mindset in a variety of ways in order to build a collaborative partnership and achieve a successful outcome. We’d like to share some of those moments, and some of the things we learned along the way.
The Initial Engagement
Our initial engagement with this client was fairly straightforward: our task was to build a user interface to display a list of vehicles and their movements through a facility. This would be an entirely new application with the goal of establishing implementation patterns for future applications.
Defining a tight, targeted scope with the client for the initial engagement was an important first step. Ensuring a small, well-defined scope helped set the engagement up for success for a number of reasons. It allowed us to
- Focus on design, architecture, quality, and code reuse
- Establish strong design, architecture, testing, and CI patterns
- Establish a relationship with the client’s team to better understand their ongoing needs
- Establish a mutually beneficial workflow, process, and cadence for deployments and releases
Defining the initial project scope was a highly collaborative process with our client. We needed to employ a consultant mindset to understand the purpose of the application from multiple perspectives. How would it solve user problems? How would it fit into the company’s greater technical landscape? What limitations existed? By exploring these questions, were we able to more deeply understand the goals of the user and the information that the user needed to achieve these goals.
For example, one main user goal was to identify equipment malfunctions which could cause path obstructions on a warehouse floor. Solving this problem required a great deal of collaboration between design, development, and stakeholders. While our development team worked with the client’s team to determine what information was available from the system to inform users, our design team worked with users and stakeholders to determine how to present that information in the most understandable and actionable way possible.
To streamline collaboration, we did a great deal of prototyping in Figma. Building out prototypes in Figma first allowed us to easily iterate on the user experience of the application. It enabled a quick feedback loop, allowing us to integrate feedback from stakeholders on usability as well as feedback from the client’s system developers on technical feasibility.
We iterated on these prototypes and tested them with potential users in order to establish a balance between user needs and technical restrictions. This process would allow us to carve out a tightly focused feature set that would compose our minimum viable product, MVP. By focusing our iteration on the prototyping phase rather than the build-out phase, we were able to identify and solve potential problems early and reduce overall development expenses.
For this engagement, we spent a lot of time with the client’s engineering director and product owner to form a deeper understanding of the company, their product domain, their technical needs, and the needs of their users. We established an open, collaborative relationship early, which allowed us to surface problems and arrive at solutions together. Let’s explore a few ways that we were able to identify and provide value through client collaboration.
Identifying Opportunities for Reuse
Some problems may be specific to a particular application, feature, or module. However in software development, we often find that problems of a similar nature exist across a company’s technical landscape. Identifying these areas is a crucial step for an organization when developing a technical architecture. By identifying, abstracting, and encapsulating these concerns, we can reduce overall system complexity and make it easier to understand and work with.
Through our conversations and work, we discovered several cross-project needs:
- A set of company-branded, accessible, reusable UI components
- A general purpose module for drawing and manipulating geographic maps of warehouse facilities
- An architecture that supports building, maintaining, and reusing these types of packages across the company’s projects
By identifying and abstracting these kinds of concerns from any specific application, we enable developers to share code across projects, while maintaining a single source of truth for any specific package. This sort of cross-organization thinking—such as taking care to understand how our work relates to the whole system and taking steps to ensure harmony between our work and the entire system—is emblematic of the consultant mindset.
Defining the Boundaries
Once we’ve identified a shared concern, we need to draw boundaries around it. There needs to be a clear, specific purpose for each concern, and we must take care to define how that concern interacts with others—like whether these concerns are modules in the same application, separate applications that communicate with each other, or libraries shared across applications, so we must ensure that their responsibilities and interfaces are well defined.
This is often easier said than done. Systems tend to evolve over time. In our case, we were discovering these needs in real-time alongside our client through the course of our regular work. In these cases, it can be helpful to first establish “conceptual” boundaries—such as boundaries in our processes and code conventions which can be fortified over time.
For example, let’s take one of the concerns we identified above: A set of branded, accessible, reusable UI components. As our client began to ramp up additional projects and teams, it became clear that a reusable component library would provide immense value across projects. By first establishing conceptual boundaries in our own application, we were able to more easily extract reusable pieces of our own code into a separate library.
How does that look in practice? To start, we need the right building blocks. Through the typical course of UI development, we often identify small, atomic components that can be reused and composed together to create larger organisms. The key here was to identify a distinction between general-purpose components and components that were specific to our application.
Once we had identified that distinction, we needed a strategy to fortify our conceptual boundary. We wanted to ensure that we treated our fledgling component library as a separate dependency from the rest of our application. This conceptual separation would allow our fledgling library to eventually leave the nest and soar on its own.
Fortifying the Boundaries
As the boundaries of a system emerge, we need to take steps to fortify and enforce them. We can do this by adopting conventions and processes that help to solidify the boundaries and interactions between them. Let’s explore how with a few examples.
Fortifying Boundaries through Convention
In the example of our component library, we first extracted our general purpose components to their own directory—still inside our existing project repo but separate from the rest of our application code. We established the rule that our application could depend on code from our component library, but never the reverse.
We then mapped that directory to a webpack alias that mimicked the name of a scoped npm package for example:
@clientname/components. This allowed us to import components from our fledgling library into our application as if they were already in a separate npm package:
import Button from '@clientname/components';
Now even though the components library code still lived in our application repo, we had established a strong conceptual boundary between the two. We enforced this further with a custom linting rule to enforce that components from the library were only imported via the webpack alias. Together, these steps would serve to fortify our boundary and ultimately allow us to extract our component library from our application repo into a package of its own.
This approach provided numerous benefits to our process. As our team worked on building the application, we now had a simple mechanism to extract reusable components and treat them as separate dependencies. This allowed us to maintain a tight feedback loop because everything still lived in the same codebase, while also making the transition into a full-fledged component library a relatively seamless process.
Fortifying Boundaries through Process
The previous example shows how we can adopt conventions to fortify emerging boundaries within an existing system. Now let’s look at another cross-cutting concern and examine how we can implement processes to further define and refine the interface between systems.
Previously we identified another concern that would be shared across multiple client applications: A general purpose, extensible package for drawing and manipulating geographic maps of warehouse facilities. This was challenging for several reasons. Not only did this package need to support our use cases, but it also needed to support use cases for other teams and be robust and extensible enough to support unanticipated uses as well. This would require a lot of iteration to refine the functionality and interface of the package.
To facilitate this, one developer on our team adopted a “feature lead” role. This developer would assume responsibility for leading the development of the package while our existing project tech lead assumed the role of the product owner. This allowed the tech lead to describe the requirements from the perspective of the consumer of the package while the features lead could take ownership of the implementation.
Adopting this sort of “micro process” within our existing team structure allowed us to fortify the boundaries of responsibilities and quickly iterate on the functionality and interface of the package. The separation of responsibilities helped us to ensure that the package was both easy to consume as well as still maintainable.
Implementing Patterns for Reuse
To compile our npm packages, as well as streamline package creation and consumption, we adopted a few tools:
- We used rollup.js to bundle our packages
- We used a versioning tool in conjunction with conventional commits to automatically handle version bumps and generate changelogs from our commit history
- We rolled in Storybook to provide high-level documentation and example uses for our components
- We used jsdoc to comment our code and generate documentation from those comments
- We leveraged the client’s existing asset management tool, Artifactory, to create a private npm registry to host our npm packages
By adopting these tools and practices, we were able to streamline the process of updating, publishing, and consuming packages. Additionally, putting these patterns in place enabled and promoted the sharing of code across teams and projects.
A Consultant Mindset Helps Our Clients Succeed Beyond Our Engagement
We love working with our clients. But we also know that at some point the party has to end. When it’s time to wrap up and say goodbye, it’s crucial that we ensure a smooth transition and leave our client set up for success.
The process of offboarding starts well before the final line of code is tested and deployed to production. Throughout our engagements, it’s crucial to ensure that we are considering the future state of the work we’re currently doing. Adopting processes like strong automated testing, thorough code reviews, and frequent refactoring helps us to ensure that the end product is maintainable and extensible for future needs.
Communication and training are also key factors in setting our clients up for success. As such, we focus heavily on writing documentation and pairing with client developers. In this particular case, we facilitated regular knowledge transfer sessions with the client’s development teams. Additionally, we were able to bring one of the client’s newer developers onto the project for the final month to ensure a smooth handoff.
Of course, this process looks different for different clients and projects. In all cases, the key to success is to maintain an open, collaborative, and curious relationship with the client.