Monorepo Insights: Nx, Turborepo, and PNPM (1/4)

Héla Ben Khalfallah
ekino-france
Published in
21 min readJul 4, 2024

Exploring the strengths and weaknesses of today’s top monorepo solutions

Monorepo Mosaic: Harmony in Code (Image licensed to the author)

Introduction

Following our successful bundlers selection study, ekino-France is diving headfirst into the world of monorepos! We’re embarking on an exciting showdown, pitting Nx, PNPM, and Turborepo against each other to determine the ultimate champion for our projects.

In this series, we’ll dive deep into the world of monorepo managers, evaluating their features, performance, and fit for our specific needs. Our goal is to find the monorepo manager that will enhance our development process, leading to more efficient workflows and simplified codebase management.

In this introductory installment, we’ll lay the groundwork by exploring the theoretical concepts underpinning monorepo managers. We’ll delve into:
· Monorepo Managers and Build Systems
Build Systems: The Engines of Transformation
Monorepo Managers: The Orchestrators of Collaboration
A Powerful Synergy
Overcoming Monorepo Challenges
· Dependency Management in Monorepos: A Graph-Based Approach
Directed Acyclic Graph (DAG)
Task Graph
Topological Sorting
Cycle Detection
Reachability Analysis
Shortest Path Algorithms
· Cache handling
Why is Caching a Game-Changer for Monorepos?
Cache Invalidation
· Tasks Scheduling
Task scheduling strategies
Real-world cases
· Modularization and Code Sharing
· Summary
· Conclusion

Fasten your seatbelts, the departure is immediate! 🚀 🌟

Monorepo Managers and Build Systems

Monorepo managers and build systems are often mistakenly conflated. However, they are distinct entities with complementary roles within a monorepo ecosystem. Understanding their relationship is key to harnessing the full power of monorepo development.

Build Systems: The Engines of Transformation

At their core, build systems are the workhorses of our codebase. They transform raw source code into executable applications or libraries through a series of automated steps:

1️⃣ Building:

✔️ Compilation/Transpilation: Converting code from one language (e.g., TypeScript) into another (e.g., JavaScript) that machines can understand.

✔️ Minification: Reducing code size through techniques like removing whitespace and renaming variables to improve loading times.

✔️ Bundling: Packaging code and assets (images, fonts, etc.) into optimized files for deployment.

2️⃣ Linting and Formatting: Enforcing consistent code style and quality standards to ensure maintainability.

2️⃣ Testing: Executing automated tests (unit, integration, end-to-end) to verify functionality and catch errors early.

https://www.hongkiat.com/blog/webpack-introduction/

Monorepo Managers: The Orchestrators of Collaboration

Monorepo managers, on the other hand, act as the conductor of our monorepo’s complex symphony. They provide the tools and structure for:

✔️ Dependency Management: Efficiently resolving and linking dependencies between projects within the monorepo, ensuring smooth collaboration and avoiding conflicts.

✔️ Task Orchestration: Coordinating the execution of build, test, and other tasks across multiple projects, taking into account their dependencies and optimizing for parallelism.

✔️ Code Sharing: Enabling the reuse of code and components across different projects in the monorepo, promoting consistency and reducing redundancy.

✔️ Tooling Integration: Seamlessly integrating with our preferred development tools (linters, formatters, test runners) for a unified experience.

✔️ Workflows and Conventions: Establishing conventions for project structure, naming, and task execution, fostering consistency and ease of maintenance.

https://monorepo.tools/

A Powerful Synergy

Monorepo managers leverage the capabilities of build systems to perform the actual transformation of code into artifacts. However, they go beyond this by providing a layer of abstraction that streamlines monorepo management.

Think of the build system as the engine of a car, and the monorepo manager as the driver. The engine provides the power, but the driver directs it, ensuring a smooth and efficient journey.

For example:

✔️ Nx: uses a powerful task runner to orchestrate builds and tests, while also offering other features specific to monorepos.

✔️ Turborepo: optimizes the execution of existing build tools (like npm scripts) through advanced caching and task pipelining.

✔️ pnpm workspaces: focuses on efficient dependency management while seamlessly integrating with the existing build scripts.

Overcoming Monorepo Challenges

Monorepos introduce unique challenges that demand thoughtful solutions from both build systems and monorepo managers. Let’s delve into these key challenges and how modern tools have addressed them:

1️⃣ Untangling Circular Dependencies:

https://www.researchgate.net/figure/Circular-dependency-chain_fig5_4283963

🔳 Problem: Circular dependencies occur when two or more projects within a monorepo depend on each other, creating a closed loop. This can lead to build failures, infinite loops, and general chaos in the development workflow.

🔳 Solution: Modern build systems and monorepo managers excel at detecting these cycles and providing solutions:

✔️ Refactoring Guidance: Suggesting ways to break the circularity by restructuring the code or dependencies.

✔️ Manual Build Ordering: Allowing us to explicitly define a build order to resolve dependencies in a controlled manner.

✔️ Dependency Inversion: Encouraging the use of design patterns like dependency inversion to reduce coupling between projects.

2️⃣ Elevating Developer Experience (DX):

🔳 Problem: Poor developer experience (DX) can hinder productivity and lead to frustration. Slow builds, cryptic error messages, and complex tooling are common culprits.

🔳 Solution: Effective build systems and monorepo managers prioritize DX by providing:

✔️ Clear Error Messages: Pinpointing the exact cause of errors with actionable suggestions.

✔️ Intuitive Interfaces: Offering user-friendly command-line interfaces (CLIs), visual tools (e.g., Nx Console), or IDE integrations for streamlined workflows.

✔️ Fast Feedback Loops: Leveraging incremental builds and caching to provide rapid feedback on code changes.

✔️ Hot Module Reloading (HMR): Enabling instant updates to running applications without full page reloads, further improving the development cycle.

3️⃣ Scaling with Project Growth:

https://www.drawio.com/blog/dependency-graphs

🔳 Problem: As monorepos grow in size and complexity, managing dependencies, orchestrating tasks, and ensuring build performance become increasingly challenging.

🔳 Solution: Robust build systems and monorepo managers offer features to tackle these scaling issues:

✔️ Intelligent Task Scheduling: Optimizing the build order and parallelizing independent tasks to reduce build times.

✔️ Advanced Caching: Storing intermediate build artifacts to avoid redundant work.

✔️ Distributed Builds: Distributing builds across multiple machines for massive projects.

✔️ Code Splitting: Breaking down large applications into smaller chunks for faster builds and improved performance.

4️⃣ Empowering Team Collaboration:

https://medium.com/@deanbiscocho/the-art-of-well-documented-code-a-developers-guide-cbc8485da9b5

🔳 Problem: In large teams, coordinating development efforts, managing shared dependencies, and avoiding conflicts can be a logistical nightmare.

🔳 Solution: Modern tooling equips teams with:

✔️ Fine-Grained Dependency Management: Allowing projects to depend on specific versions of shared libraries to avoid breaking changes.

✔️ Parallel Development: Enabling developers to work on different parts of the monorepo concurrently without stepping on each other’s toes.

✔️ Code Review Workflows: Integrating with code review tools to ensure code quality and consistency.

✔️ Release Management: Streamlining the process of publishing and deploying multiple projects.

Now that we’ve explored the common hurdles of monorepo development, let’s dive deeper into how monorepo managers efficiently manage dependencies. 🌟

Dependency Management in Monorepos: A Graph-Based Approach

Managing dependencies in a monorepo can feel like untangling a giant ball of yarn, leading to circular dependencies, version conflicts, and slow build times. But what if there was a better way?

In this section, we will explore how graph theory can help to visualize, analyze, and optimize the monorepo’s dependency structure, leading to a more efficient and enjoyable development experience. Here we go! ✈️

Directed Acyclic Graph (DAG)

🔳 At the heart of many monorepo managers lies a powerful tool from graph theory: the Directed Acyclic Graph (DAG). This structure elegantly represents the complex web of dependencies between projects, ensuring a smooth and efficient development process:

In mathematics, particularly graph theory, and computer science, a directed acyclic graph (DAG) is a directed graph with no directed cycles. That is, it consists of vertices and edges (also called arcs), with each edge directed from one vertex to another, such that following those directions will never form a closed loop. — https://en.wikipedia.org/wiki/Directed_acyclic_graph

🔳 The definition mentioned earlier emphasizes the two primary characteristics of DAGs:

✔️ Directed: the edges in the graph have a direction, indicating that one project depends on another. If project A depends on project B, there’s an arrow (edge) pointing from node A to node B.

✔️ Acyclic: the graph doesn’t contain cycles. This means we cannot start at a node, follow a path along the edges, and end up back at the same starting node. In project terms, this ensures that no circular dependencies exist (e.g., project A depends on B, B depends on C, and C depends on A).

https://www.researchgate.net/figure/A-directed-acyclic-graph-DAG-representing-a-possible-causal-model-for-the-underlying_fig1_369413899

🔳 Directed Acyclic Graphs (DAGs) are useful for a variety of reasons due to their specific characteristics:

✔️ Representing Dependencies: DAGs are excellent at representing dependencies between objects or events. The directed edges clearly show which item depends on which other item, and the acyclic nature ensures there are no circular dependencies that could lead to conflicts or infinite loops.

✔️ Modeling Workflows and Processes: DAGs can model complex workflows and processes where tasks have specific dependencies on one another. This ensures tasks are executed in the correct order and resources are utilized efficiently.

✔️ Data Processing and Scheduling: In data processing pipelines, DAGs can represent the flow of data transformations and computations, ensuring that each step is executed after its dependencies are met. They are also used in scheduling systems to determine the order in which tasks should be executed based on their dependencies and priorities.

✔️ Version Control and History: DAGs are used in version control systems like Git to track the history of code changes. Each commit is a node in the DAG, and the edges represent the parent-child relationships between commits.

✔️ Build Systems: Build tools like Gradle, and Bazel use DAGs to determine the order in which to compile and link source code files, ensuring that dependencies are built before they are needed.

🔳 To navigate and organize DAGs effectively, specific algorithms are employed:

✔️ Topological sorting is a key algorithm that linearizes a DAG by determining an order in which vertices can be visited such that for every directed edge from vertex u to v, vertex u comes before v. This ordering ensures that dependencies are met before a task is executed.

https://algodaily.com/lessons/staying-on-top-of-topological-sort

✔️ Other algorithms like Depth-First Search (DFS) and Breadth-First Search (BFS) can also be used to traverse DAGs. While DFS explores a single path as deeply as possible before backtracking, BFS visits all the immediate neighbors of a vertex before moving on to their neighbors. These algorithms are helpful for tasks like cycle detection or finding all reachable nodes from a given starting point.

https://www.geeksforgeeks.org/difference-between-bfs-and-dfs/

While DAGs excel at representing relationships, task graphs go beyond structure to capture the actions that need to happen in a monorepo. Let’s explore how. 🚂

Task Graph

🔳 While Directed Acyclic Graphs (DAGs) effectively represent the static dependencies between packages in a monorepo, task graphs take it a step further. They model the dynamic actions and workflows required to build, test, and deploy our code.

With task graphs you can automatically run sequences of tasks. A task graph, or directed acyclic graph (DAG), is a series of tasks composed of a root task and child tasks, organized by their dependencies. Task graphs flow in a single direction, meaning a task later in the series cannot prompt the run of an earlier task. Each task can depend on multiple other tasks and won’t run until they all complete. Each task can also have multiple child tasks that depend on it. — https://docs.snowflake.com/en/user-guide/tasks-graphs

🔳 A task graph is a specialized type of DAG where:

✔️ Nodes: Represent individual tasks or operations, such as compiling a file, running tests for a specific package, or deploying a service.

✔️ Edges: Define the dependencies between tasks, indicating which tasks must be completed before others can start. If task A must be completed before task B can start, an edge points from A to B.

https://nlguillemot.wordpress.com/2017/01/13/using-cont-with-tbbtask_group/

🔳 Monorepo managers use task graphs to orchestrate complex workflows. Here’s how it typically works:

✔️ Graph Generation: When we run a command (like nx build myapp), the monorepo manager analyzes the project's configuration and source code to determine the tasks needed and their dependencies. This information is used to construct the task graph.

✔️ Topological Sorting: The manager then applies topological sorting (we’ll see it later) to the task graph, establishing a valid order in which tasks can be executed. This ensures that dependencies are met before a task is started.

✔️ Task Execution: The manager executes tasks in the topologically sorted order. It can often run independent tasks in parallel, significantly speeding up the process.

✔️ Caching: Many monorepo managers leverage caching to avoid repeating work. If a task’s inputs (source code, dependencies) haven’t changed, its cached output can be reused.

🔵 Let’s consider a simplified monorepo with two projects:

  • ui-components: A library of UI components
  • my-app: An application that uses ui-components

The task graph for building my-app might look like this:

build(ui-components) --> lint(ui-components) --> test(ui-components) --> build(my-app) --> lint(my-app) --> test(my-app)

🔳 To shed light on the nuanced interplay between dependency graphs and task graphs, and how they complement each other in monorepo management, let’s examine this comparison table:

DAGs vs. Task Graphs (Image by the author)

While DAGs and task graphs elegantly model dependencies, they don’t inherently provide a clear execution order. That’s where topological sorting steps in, transforming these complex structures into actionable sequences that guide the monorepo build process. Let’s see! 🚁

Topological Sorting

🔳 In the realm of Directed Acyclic Graphs (DAGs), topological sorting is a pivotal algorithm that transforms the abstract web of dependencies into a concrete, actionable sequence:

https://www.naukri.com/code360/library/topological-sorting

🔳 It’s the process of ordering the vertices (nodes) of a DAG such that for every directed edge from vertex u to v, vertex u comes before v in the ordering. Or, in simpler terms, it arranges tasks based on their dependencies, ensuring that a task is not started until all its prerequisites are finished.

Let’s take a closer look at this miniature example:

Package A depends on Package B and Package C
Package B depends on Package D
Package C has no dependencies

A topological sorting of this graph would be: D -> B -> C -> A. This means that Package D must be built first, followed by B, C, and finally A.

🔳 As you can see, there can be multiple valid topological orders for the same Directed Acyclic Graph (DAG). In the context of monorepo management, this means there might be multiple valid build orders for our packages. The specific order chosen might depend on factors like:

✔️ Parallelism: some build tools might prioritize an ordering that maximizes the potential for parallel execution of tasks.

✔️ Optimization: the chosen order might be optimized to minimize build times or resource usage.

✔️ Customization: some tools might allow us to specify preferences or constraints to influence the topological sorting algorithm’s output.

💡 The ability to have multiple valid topological orderings for a DAG can be likened to a form of adaptive management. Just like machine learning (ML) models learn and adapt their behavior based on data, monorepo managers can utilize different topological orderings to optimize build processes or respond to changing project needs.

🔳 Also, in the context of monorepos, topological sorting is crucial for:

✔️ Build Order: Determining the correct order in which to build or compile packages based on their dependencies.

✔️ Task Execution: Scheduling tasks (e.g., linting, testing, deployment) in a way that respects dependencies between them.

✔️ Dependency Analysis: Identifying potential issues like circular dependencies (which would make topological sorting impossible).

✔️ Caching: Optimizing build caches by ensuring that a package is rebuilt only when its dependencies change.

While topological sorting provides a solid foundation for understanding dependency relationships, monorepo managers often employ more specialized graph structures to represent the complex interplay of tasks and their associated metadata.

🔵 Example: Imagine a monorepo workspace with the following projects and dependencies:

apps:

  • store-ui (a storefront web application)
  • admin-ui (an admin dashboard web application)

libs:

  • shared-ui (UI components shared by both applications)
  • product-data-access (data fetching logic for products)
  • cart-data-access (data fetching logic for shopping carts)
  • auth (authentication library)
  • utils (utility functions)

The dependencies between these projects can be visualized in a DAG as follows:

store-ui --> shared-ui
store-ui --> product-data-access
store-ui --> cart-data-access
store-ui --> auth
admin-ui --> shared-ui
admin-ui --> auth
shared-ui --> utils
product-data-access --> utils
cart-data-access --> utils
auth --> utils
https://python-code-aws.trinket.io/python-generated/2cshue5a/trinket_plot.png

The Python program used to draw this graph is here. And here is the Python code for performing a topological sort on this Directed Acyclic Graph (DAG). The topological sorting order of the graph is:

Topological Sorting Order:
[
'store-ui',
'admin-ui',
'product-data-access',
'cart-data-access',
'shared-ui',
'auth',
'utils'
]

This order ensures that each node appears before all the nodes it points to, representing a valid sequence of dependencies and it reflects the valid dependencies within the project graph and ensures that each project is built only after its dependencies are met.

While topological sorting is a powerful tool, it hinges on a crucial assumption: the absence of cycles within the graph. Let’s now explore how monorepo managers ensure this validity by employing cycle detection algorithms. ♻️

Cycle Detection

Cycle detection is the process of identifying these circular dependencies within a directed graph. It’s a critical step in maintaining the integrity of the monorepo and ensuring a smooth development experience.

https://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

🔳 Why is Cycle Detection Important in Monorepos?

✔️ Prevents Build Failures: Circular dependencies make it impossible to determine a valid build order, leading to errors and frustration.

✔️ Maintains Codebase Integrity: Cycles can signal design flaws or architectural issues within the codebase. Identifying them early helps maintain a healthy and maintainable code structure.

✔️ Facilitates Dependency Management: By detecting cycles, we can refactor our code to eliminate them, leading to cleaner and more manageable dependencies.

✔️ Enables Topological Sorting: Topological sorting, crucial for determining build order, is only possible in directed acyclic graphs (DAGs). Cycle detection ensures that our dependency graph remains acyclic.

🔳 Several algorithms exist for cycle detection, but a common approach is based on Depth-First Search (DFS):

+-----------+         +-------------+        +-------------+       +-------------+
| Start | -----> | Mark as | -----> | Check for | ----->| Repeat |
| at Node | | Visited | | Back Edges | | (if needed) |
+-----------+ +-------------+ +-------------+ +-------------+
| | | |
| | | |
v v v v
(A) (A) Visited (A) --------> (B) Visited
^ |
| | (Back Edge)
| |
(C) Visited
  • Visited Flag: A crucial part of the algorithm is marking nodes as visited to avoid infinite loops and track the current exploration path.
  • Back Edge Detection: The key to detecting cycles is identifying edges that lead back to nodes on the current exploration path (before backtracking).

🔳 Many monorepo tools have built-in cycle detection mechanisms:

✔️ Nx: The nx dep-graph command can visualize dependencies and highlight cycles.

✔️ Turborepo: Turborepo automatically detects and reports circular dependencies.

Cycle detection is an indispensable part of monorepo management. By identifying and resolving circular dependencies, we ensure the stability and maintainability of our codebase, paving the way for a smoother and more efficient development process.

While ensuring a cycle-free graph is paramount, monorepo managers also need to understand the connections between packages. This is where reachability analysis comes into play, revealing which parts of the system are impacted by changes in others (affected). Whoa! 🌟

Reachability Analysis

🔳 Reachability analysis, in the context of graphs, refers to the process of determining whether a particular node (e.g., a package in a monorepo) can be reached by traversing the edges of the graph from another given node. In simpler terms, it helps us answer the question: “If I change this, what else is impacted?”

🔳 Why is Reachability Analysis Important in Monorepos?

✔️ Efficient Rebuilds: In large monorepos, rebuilding the entire codebase for every minor change is impractical. Reachability analysis allows to pinpoint the specific packages that depend on the changed code, enabling targeted and faster rebuilds.

✔️ Targeted Testing: Similarly, reachability analysis helps identify the tests that need to be re-run when a package is modified. This optimizes the testing process and saves valuable time.

✔️ Impact Assessment: Before making significant changes to a package, reachability analysis can reveal the potential consequences. This helps to assess the risk and plan accordingly.

✔️ Dependency Visualization: Reachability analysis can be used to generate visual representations of the dependency relationships between packages, making it easier to understand the overall structure of the monorepo.

🔳 Reachability analysis in monorepos typically utilizes graph traversal algorithms like Depth-First Search (DFS) or Breadth-First Search (BFS).

🔳 Several monorepo tools offer features to facilitate reachability analysis:

✔️ Nx: The nx affected:dep-graph command visualizes the dependency graph of the affected projects, highlighting the dependencies between them.

✔️ Turborepo: Turborepo intelligently determines which tasks need to be re-executed based on the changed files, indirectly performing a form of reachability analysis.

✔️ Custom Scripts: We can also write custom scripts that leverage graph algorithms to perform reachability analysis on our monorepo’s dependency graph.

https://github.com/leanix/nx-affected-dependencies-action

Reachability analysis is a powerful tool in the monorepo toolbox!

While reachability analysis helps us understand which tasks are affected by changes, finding the most efficient way to execute those tasks is equally crucial. This is where shortest path algorithms come in, enabling monorepo managers to optimize the build process by identifying the critical path within the task graph. Whoa! 🔥

Shortest Path Algorithms

🔳 Shortest path algorithms are used to find the most efficient path between two nodes in a graph.

https://www.researchgate.net/figure/Tree-constructed-by-Extended-Dijkstras-Shortest-Path-Algorithm_fig2_341215943

🔳 In the context of monorepos, finding the shortest path in a task graph can significantly optimize the build process:

✔️ Minimize Build Time: By identifying the critical path (the longest chain of dependent tasks), monorepo managers can prioritize those tasks and parallelize independent tasks, reducing the overall build time.

✔️ Resource Optimization: Shortest path algorithms can help allocate resources efficiently by identifying bottlenecks and focusing computational power where it’s most needed.

✔️ Improved Developer Experience: Faster builds mean quicker feedback cycles for developers, leading to increased productivity and a smoother development experience.

🔳 Popular short path algorithms include Dijkstra’s Algorithm and Bellman-Ford Algorithm.

Shortest path algorithms play a vital role in optimizing monorepo workflows by identifying bottlenecks, prioritizing critical tasks, and enabling parallel execution!

With a firm grasp of how graph theory underpins dependency management in monorepos, let’s now shift gears and explore another crucial optimization technique: caching. 🔮

Cache handling

Why is Caching a Game-Changer for Monorepos?

Without caching, building a monorepo can be painfully slow. Each time we make a change, even a minor one, the entire project or a large portion of it might need to be rebuilt.

🔳 Caching provides a solution by storing the results of previous builds and intelligently reusing them:

✔️ By avoiding redundant work, caching can significantly reduce build times.

✔️ Faster builds mean quicker feedback loops for developers.

✔️ Caching minimizes the need to re-run computationally expensive tasks, freeing up valuable CPU cycles and memory for other processes.

✔️ Continuous Integration and Continuous Deployment (CI/CD) pipelines often involve multiple builds and tests. Caching can significantly speed up these processes, enabling faster releases and more frequent deployments.

🔳 To optimize build performance in monorepos, different caching mechanisms can be employed, each with unique advantages and tradeoffs:

Caching mechanisms (Image by the author)
https://nx.dev/concepts/how-caching-works
https://nx.dev/concepts/how-caching-works

🔳 Companies like Google, Facebook, and Uber have embraced monorepos, and caching plays a pivotal role in their success. These companies have reported massive reductions in build times and significant improvements in developer productivity thanks to intelligent caching strategies.

https://qeunit.com/blog/how-google-does-monorepo/

We’ve explored the power of caching, but there’s one more piece of the puzzle to uncover: cache invalidation. This critical process keeps the cache relevant and ensures that builds are always fresh and reliable. 👷

Cache Invalidation

Cache invalidation is a process in a computer system whereby entries in a cache are replaced or removed. It can be done explicitly, as part of a cache coherence protocol. In such a case, a processor changes a memory location and then invalidates the cached values of that memory location across the rest of the computer system. — https://en.wikipedia.org/wiki/Cache_invalidation

🔳 As the codebase evolves, the cached artifacts from previous builds can become stale and outdated. If these stale artifacts are reused, it can lead to:

✔️ Incorrect Builds: The build might produce incorrect results due to outdated dependencies or changes in the codebase.

✔️ Unexpected Errors: Stale cached artifacts can cause unexpected errors and inconsistencies in the application.

✔️ Wasted Time: If we don’t know about the stale cache, we could end up debugging issues that could have been prevented.

🔳 Monorepo managers employ various strategies to intelligently invalidate cache entries when changes occur:

Cache invalidation strategies (image by the author)

Modern monorepo managers employ sophisticated algorithms to analyze dependencies, track changes, and intelligently invalidate cache entries. This minimizes unnecessary rebuilds while ensuring that our builds are always accurate and up-to-date.

Having tackled the challenge of cache freshness, we’re now ready to dive into another key optimization technique: task scheduling. By strategically ordering and executing tasks, monorepo managers can unlock even greater speed and efficiency gains. 💎

Tasks Scheduling

Tasks scheduling strategies

To provide a clear overview, let’s summarize the various task scheduling strategies employed in monorepos in the following table:

Task scheduling strategies (Image by the author)
https://patterns.eecs.berkeley.edu/?page_id=609

💡 The choice of task scheduling strategy depends on various factors, including the size and complexity of the monorepo, the available resources, and the specific requirements of the project.

Real-world cases

✔️ Nx: Primarily uses topological task scheduling with parallelization and caching. It also allows for custom task executors that can implement more specialized scheduling strategies.

✔️ Turborepo: Heavily emphasizes parallel task scheduling and caching. It uses a directed graph to model task dependencies and intelligently schedules tasks based on their relationships.

✔️ Bazel: Employs a sophisticated task scheduling system based on a directed graph of build actions. It supports parallel execution, caching, and incremental builds.

Our builds are now optimized, but what steps do we need to take to ensure that our monorepo remains maintainable and scalable as it grows? The answer can be found in the principles of modularization and code sharing. Let’s dive into! 🎯

Modularization and Code Sharing

A well-structured monorepo is built upon a foundation of modularity and efficient code sharing. By organizing code into cohesive units and promoting reuse, we can create a maintainable, scalable, and flexible codebase.

https://www.toptal.com/front-end/guide-to-monorepos

To understand how modularization and code sharing are implemented effectively in monorepos, let’s explore these key concepts and their practical applications:

Modularization and code sharing (Image by the author)

The investment in well-structured modularization and code sharing practices pays off in the long run. It not only streamlines our immediate development workflow but also sets the stage for a more adaptable, resilient, and future-proof monorepo architecture. 👏

We’ve covered a lot of ground, from dependency graphs to caching strategies. Now, let’s distill this knowledge into a concise summary, highlighting the most critical aspects to keep in mind when evaluating monorepo managers. Whoa!

Summary

Based on what we’ve explored, choosing the right monorepo manager boils down to a few key considerations:

✔️ Graph Type: Understanding whether the tool uses a dependency graph, task graph, or both is crucial for evaluating its capabilities in managing project relationships and orchestrating build processes.

✔️ Task Scheduling: The scheduling approach (topological, parallel, incremental, etc.) directly impacts build times and resource utilization, making it a vital consideration for optimizing the monorepo’s performance.

✔️ Caching Mechanism: A robust caching strategy is essential for accelerating builds, especially in large monorepos. Consider the type of caching (file-based, content-addressable, distributed) and how it aligns with the team’s workflow and infrastructure.

✔️ Modularization and Code Sharing: Assess the tool’s support for modular code organization and code sharing mechanisms. Does it facilitate the creation of well-defined modules and promote code reuse within the monorepo?

✔️ Additional Considerations: Don’t forget about factors like community support, documentation, licensing costs, and integration with the existing tech stack and build tools. These aspects can significantly impact the overall experience and the long-term success of the monorepo.

Our journey through the intricate landscape of monorepo management has reached its destination. Let’s move towards the conclusion! 🌼

Conclusion

What an amazing journey full of knowledge this has been!

We’ve explored the theoretical foundations that underpin monorepo management, from the elegant structures of directed acyclic graphs to the intricate algorithms that orchestrate build processes.

These concepts — dependency resolution, caching strategies, task scheduling, modularization, and code sharing — are not merely academic curiosities; they are the building blocks that empower us to create efficient, scalable, and maintainable monorepos.

Armed with this newfound understanding, we are now prepared to embark on a practical exploration of the leading monorepo managers: Nx, Turborepo, and PNPM Workspace.

In our upcoming showdown, we’ll see how these tools translate theory into practice, each offering unique solutions to optimize your monorepo workflow.

I’d like to express my sincere gratitude to the brilliant minds who developed the graph theory concepts and algorithms we’ve explored. It’s incredibly rewarding to witness how these theoretical constructs have become indispensable tools in modern software development, particularly in the realm of monorepos.

Stay tuned! 🍀 🌻

Until we meet again in a new article and a new adventure! ❤️

Thank you for reading my article.

Want to Connect? 
You can find me at GitHub: https://github.com/helabenkhalfallah

--

--

Héla Ben Khalfallah
ekino-france

Hi! I'm Hela Ben Khalfallah, Senior Frontend Expert specializing in JavaScript, React, Node.js, software architecture, and FrontendOps.