Build better software to build software better
We manage the build pipeline that delivers Quip and Slack Canvas’s backend. A year ago, we were chasing exciting ideas to help engineers ship better code, faster. But we had one huge problem: builds took 60 minutes. With a build that slow, the whole pipeline gets less agile, and feedback doesn’t come to engineers until…

Build Better Software to Build Software Better
At Quip and Slack Canvas, we manage the build pipeline that delivers the backend for our products. A year ago, we were eager to explore innovative ideas that could help engineers ship better code faster. However, we faced a significant challenge: our builds took 60 minutes. With such a slow build process, the entire pipeline became less agile, and engineers had to wait far too long for feedback. We needed to address this issue to improve our development workflow.
To resolve the problem, we combined modern, high-performance build tooling with classic software engineering principles. We started by rethinking how we approached build performance, drawing parallels to optimizing application code.
Imagine a simple application with a backend server providing an API and data storage, and a frontend presenting the user interface. Like many modern applications, the frontend and backend are decoupled, allowing them to be developed and delivered independently. The build process for such an application can be represented as a graph of dependencies between elements, such as source files and deployable artifacts.
We modeled our build as a directed acyclic graph (DAG), where arrows represent dependencies. For instance, our backend depends on a collection of Python files, meaning that whenever a Python file changes, we need to rebuild the backend. Similarly, we need to rebuild the frontend whenever a TypeScript file changes—but not when a Python file does. This approach allows us to apply performance optimization techniques similar to those used in optimizing application code.
One key technique is to do less work. By caching the results of expensive computations, we ensure that we only perform them once, trading memory for time. This is analogous to memoization in code, where we store the results of function calls to avoid redundant calculations.
Another technique is to share the load. We can distribute work across multiple compute resources in parallel, enabling faster completion by trading off compute resources for time. This is similar to parallel processing in code, where tasks are divided among multiple threads or processes to speed up execution.
For example, consider a Python function to calculate the factorial of a number:
```python
def factorial(n):
return n * factorial(n-1) if n else 1
```
Calculating a factorial can be computationally expensive. If we need to compute the factorial multiple times with the same input, we can cache the result to avoid redundant calculations. This not only improves performance but also reduces resource usage.
Applying these principles to our build pipeline, we began by analyzing the dependencies and identifying bottlenecks. We then optimized our build process using caching and parallelization techniques. By leveraging Bazel, a high-performance build tool, we were able to significantly reduce build times.
Bazel's architecture allows for efficient dependency resolution and parallel execution of tasks. It also supports caching, ensuring that only the necessary parts of the build are re-executed when changes occur. This combination of features enabled us to achieve substantial improvements in build performance.
In addition to using Bazel, we also applied classic software engineering principles. We refactored our build scripts to improve maintainability and readability, ensuring that our team members could easily understand and modify them as needed. We also established clear guidelines for how developers should structure their code and build configurations, promoting consistency and reducing the likelihood of build-related issues.
By focusing on performance and adopting a systematic approach, we transformed our build pipeline. Today, our builds are much faster, enabling engineers to receive feedback more quickly and iterate on their code more efficiently. This improvement has had a ripple effect throughout our development process, fostering a more agile and productive workflow.
In conclusion, optimizing build performance is crucial for modern software development. By thinking about builds in terms of code performance and applying well-established software engineering principles, we were able to significantly reduce build times and enhance our development process. The key takeaway is that building better software often begins with optimizing the tools and processes that support the build pipeline itself.










