Why Structure Matters in a Monorepo

The structure of a monorepo impacts developer productivity and the overall quality of the codebase. A well-organized monorepo enables teams to:

  • Maintain Consistency: Developers can follow a standardized layout, reducing confusion.
  • Improve Code Reusability: Shared libraries are easily accessible and can be reused across projects.
  • Enhance Build Performance: Proper structure enables tools to optimize builds and testing selectively.
  • Simplify Dependency Management: Clear project boundaries help prevent dependency conflicts and circular dependencies.

To achieve these benefits, it’s important to adopt a logical structure that suits your team’s needs and growth.

Basic Directory Structure for a Monorepo

A monorepo generally has three main categories of directories:

  • Applications: Contains the individual applications or projects within the monorepo.
  • Libraries: Houses reusable code, modules, or components shared across applications.
  • Tools and Configurations: Stores configurations, scripts, and other tooling to support the entire monorepo.

A common directory layout might look like this:

/apps
  ├── app1
  ├── app2
/libs
  ├── shared-lib1
  ├── shared-lib2
/configs
/scripts

This basic layout establishes clear boundaries between applications and shared code, promoting modularity and reusability.

Best Practices for Directory Organization

1. Group Projects by Domain or Functionality

When structuring a monorepo, group applications and libraries by domain or functionality rather than arbitrary categories. For example:

/apps
  ├── billing
  ├── user-management
/libs
  ├── ui-components
  ├── utils
  ├── api-services

This organization improves navigability by keeping related code together, making it easier for developers to find and manage code specific to each domain.

2. Separate Applications from Libraries

Keeping applications and libraries in separate directories is essential for distinguishing between project-specific code and reusable code. This separation helps maintain a modular structure, where applications rely on libraries for shared functionality.

3. Use Clear Naming Conventions

Consistent and descriptive naming conventions make it easier to identify and navigate projects within a monorepo. For instance:

  • Use singular names for directories (e.g., api-service rather than api-services).
  • Prefix shared libraries with lib or shared to indicate their reusability.
  • Use domain-specific prefixes, like user- for user-related components or billing- for billing-related code.

Clear naming conventions improve readability and reduce confusion about the purpose of each directory or file.

Organizing Shared Libraries in a Monorepo

Shared libraries are one of the main advantages of using a monorepo, as they allow teams to consolidate reusable components, utilities, and services. Structuring shared libraries properly is critical for keeping code maintainable and reducing redundancy.

Types of Shared Libraries

Shared libraries generally fall into these categories:

  • UI Components: Reusable interface components, such as buttons, form fields, and modals.
  • Utilities: Common utility functions like date formatting, string manipulation, and API helpers.
  • Data Models: Shared data types or models that multiple applications use.

For example:

/libs
  ├── ui
  │   ├── button
  │   ├── modal
  ├── utils
  ├── models

This layout ensures clear separation between different types of shared code, making it easier to locate and manage libraries based on functionality.

Managing Dependencies Between Libraries

To avoid dependency conflicts and circular dependencies, it’s essential to set boundaries between libraries. Each library should only depend on lower-level libraries within its category or domain. Tools like Nx and Lerna provide dependency constraints, helping prevent circular dependencies and keep the codebase modular.

Using a Domain-Driven Design (DDD) Structure

Adopting a domain-driven design (DDD) structure can be highly effective in organizing monorepos. DDD categorizes code based on business domains, making it easy to understand where code fits within the overall system.

An example DDD layout might look like this:

/apps
  ├── billing
  ├── user-management
/libs
  ├── billing
  │   ├── invoice
  │   ├── transactions
  ├── user
  │   ├── authentication
  │   ├── profile

This structure helps enforce clean boundaries between different parts of the application, simplifying dependency management and promoting encapsulation.

Implementing Configuration and Build Tools

Configuration and build tools should be placed in a centralized location to avoid duplicating configurations across projects. This approach allows all applications to share consistent configurations and makes it easier to modify settings across the entire monorepo.

Centralizing Configuration Files

Store all configuration files in a single configs or tools directory:

/configs
  ├── eslint.json
  ├── prettier.json
  ├── tsconfig.json

This layout centralizes configuration files, ensuring all applications follow consistent rules and standards.

Using Build and Testing Scripts

Include shared build and testing scripts in the root or a dedicated /scripts directory to avoid duplication. Many monorepo tools, like Nx and Bazel, allow you to define scripts that can run across multiple projects:

/scripts
  ├── build.sh
  ├── test.sh
  ├── lint.sh

Shared scripts reduce maintenance overhead by allowing updates to be made in one place, which then applies to all projects.

Establishing Standards for Code Ownership and Access

Defining ownership and access standards is important in monorepos, especially in large teams. This structure ensures that teams know who is responsible for which parts of the codebase, helping to prevent accidental changes and improve accountability.

Setting Ownership Boundaries

Assign each application or library to a specific team. This can be documented within the code or a README file in each directory:

/apps
  ├── billing (owned by Team Billing)
  ├── user-management (owned by Team User)

Clear ownership boundaries reduce conflicts and improve code quality by ensuring changes are reviewed by the appropriate team members.

Controlling Access with Permissions

Some monorepo management tools allow you to set permissions, limiting who can modify certain parts of the code. This feature can prevent unintended changes, particularly in sensitive areas such as core libraries.

Automating Testing and Build Workflows

Automating testing and build workflows in a monorepo ensures code quality and consistency across projects. Many monorepo tools, like Nx, Bazel, and Lerna, support automated workflows that enable selective testing and optimized builds.

Selective Builds and Tests

Instead of rebuilding and testing the entire monorepo after every change, use selective builds and tests to target only the affected packages:

lerna run test --scope=my-package

This command limits testing to the my-package package, reducing build times and enhancing CI/CD performance.

Continuous Integration and Deployment (CI/CD)

Integrate CI/CD pipelines to automate testing, linting, and deployment across projects. CI/CD configurations should be stored in a shared location to ensure all projects follow the same workflow:

/ci
  ├── build.yml
  ├── deploy.yml

Shared CI/CD configurations simplify pipeline maintenance and ensure consistency across the entire codebase.

Documentation and README Files

Documentation is critical in large codebases. Each application and library should contain a README file that outlines:

  • Purpose: A brief description of the module’s function.
  • Setup Instructions: Steps to install and use the module.
  • Dependencies: Required packages or libraries.

Clear documentation improves onboarding for new developers and helps existing team members navigate the codebase.

Conclusion

Structuring a monorepo properly is essential for managing large codebases and ensuring team productivity. By adopting a well-organized directory structure, establishing clear naming conventions, and centralizing configuration files, teams can create a scalable and maintainable monorepo. Following best practices like grouping projects by domain, separating applications from libraries, and automating workflows improves code organization and makes the codebase easier to navigate.

Whether you’re building a monorepo for a small team or managing an enterprise-scale codebase, these best practices will help you create a structure that supports collaboration, code reusability, and efficient development workflows.