Monorepo Patterns with cmod Workspaces

As C++ projects grow, they naturally split into multiple libraries, applications, and test suites. Managing these as separate repositories creates dependency hell. Managing them as a single monolithic build creates a different kind of hell. cmod workspaces provide a middle path: a monorepo structure with clear boundaries between components.

What Is a cmod Workspace?

A workspace is a collection of cmod members (libraries and binaries) that share a single cmod.lock and can depend on each other via path dependencies. Think of it as Cargo workspaces for C++.

my_project/
├── cmod.toml          # Workspace root
├── cmod.lock          # Shared lockfile
├── core/
│   ├── cmod.toml      # Library member
│   └── src/
├── networking/
│   ├── cmod.toml      # Library member
│   └── src/
├── app/
│   ├── cmod.toml      # Binary member
│   └── src/
└── tests/
    ├── cmod.toml      # Test member
    └── src/

Setting Up a Workspace

# Initialize the workspace
cmod init my_project --workspace
cd my_project

# Add members
cmod workspace add core
cmod workspace add networking
cmod workspace add app
cmod workspace add tests

The root cmod.toml declares the workspace:

[workspace]
members = [
    "core",
    "networking",
    "app",
    "tests"
]

Each member has its own cmod.toml with its specific dependencies and configuration.

Pattern 1: The Layered Architecture

The most common monorepo pattern is layered dependencies, where higher-level components depend on lower-level ones:

# core/cmod.toml — no internal deps
[package]
name = "core"
version = "0.1.0"

[module]
name = "com.github.org.project.core"
root = "src/core.cppm"

[build]
type = "library"

# networking/cmod.toml — depends on core
[dependencies]
core = { workspace = true }
"github.com/some/tls-lib" = "^2.0"

# app/cmod.toml — depends on both
[dependencies]
core = { workspace = true }
networking = { workspace = true }
"github.com/fmtlib/fmt" = "^10.0"

The { workspace = true } syntax tells cmod to resolve the dependency from within the workspace. cmod automatically figures out the correct path and build order.

Pattern 2: Shared Dependencies

One of the biggest advantages of workspaces is unified dependency resolution. When multiple members depend on the same external library, the workspace ensures they all use the same version:

# Both core and app depend on fmt
# core/cmod.toml
[dependencies]
"github.com/fmtlib/fmt" = ">=10.0"

# app/cmod.toml
[dependencies]
"github.com/fmtlib/fmt" = ">=10.1"

# Resolution: workspace resolves to fmt 10.2.1 (satisfies both)
# Only one copy is compiled and cached

Without workspaces, each project would resolve independently, potentially getting different versions and definitely compiling fmt twice.

Pattern 3: The Test Harness

A dedicated test member that depends on all other members for integration testing:

# tests/cmod.toml
[package]
name = "integration-tests"
version = "0.1.0"

[dependencies]
core = { workspace = true }
networking = { workspace = true }
app = { workspace = true }

[build]
type = "binary"  # Test binary

Run all tests across the workspace:

# Build and test everything
cmod test

# Test only the networking member
cmod test -p networking

# Build everything, test nothing
cmod build

Pattern 4: Plugin Architecture

For projects with a plugin system, the workspace can contain the core engine and several plugins:

game_engine/
├── cmod.toml
├── engine/           # Core engine library
├── renderer-vulkan/  # Vulkan rendering plugin
├── renderer-metal/   # Metal rendering plugin
├── physics/          # Physics plugin
├── audio/            # Audio plugin
└── editor/           # Editor application

Each plugin depends on the engine's public API but not on other plugins. The editor depends on everything. This structure enforces architectural boundaries that would be easy to violate in a flat project.

Workspace Build Behavior

When you run cmod build at the workspace root:

  1. cmod loads the workspace manifest and discovers all members
  2. Each member's dependencies are resolved against the shared lockfile
  3. The combined dependency graph (workspace members + external deps) is constructed
  4. Compilation proceeds in topological order
  5. BMIs and object files are shared across members

The shared BMI cache is key: if core and networking both import fmt, the fmt BMI is compiled once and reused. This can significantly reduce build times for large workspaces.

Managing Workspace Dependencies

# Add a dependency to a specific member
cd networking
cmod add github.com/some/lib@2.0

# List workspace members
cmod workspace list
  core         v0.1.0  library
  networking   v0.1.0  library
  app          v0.1.0  binary
  tests        v0.1.0  binary

# Visualize cross-member dependencies
cmod deps --tree
  app v0.1.0
  ├── core v0.1.0 (workspace)
  ├── networking v0.1.0 (workspace)
  │   └── core v0.1.0 (workspace)
  └── fmt v10.2.1 (github.com/fmtlib/fmt)

# Remove unused deps across workspace
cmod tidy --apply

CI Strategies for Workspaces

Full workspace build (recommended for most teams)

# CI pipeline
cmod build --locked --release
cmod test --locked --release
cmod verify

Affected-member builds (for very large workspaces)

For workspaces with dozens of members, you can build only what's affected by a change:

# Build only members affected by changes in core/
cmod build -p core -p networking -p app --locked

Best Practices

  • Keep members focused: Each member should have a single, clear responsibility. If a member is doing too much, split it.
  • Minimize cross-member dependencies: The dependency graph between members should be a clean DAG. Circular dependencies are not allowed.
  • Share the lockfile: The workspace lockfile (cmod.lock) should be committed to version control. It ensures all members resolve to the same dependency versions.
  • Version members together: For simplicity, keep all members at the same version. If they need independent versioning, consider separate repositories.
  • Use workspace-level commands: Run cmod build, cmod test, and cmod clean from the workspace root to operate on everything at once.

When to Use a Workspace vs. Separate Repos

Use a workspace when:

  • Components are developed and released together
  • There are frequent cross-component changes
  • You want unified dependency resolution
  • The team is small enough to own the entire codebase

Use separate repositories when:

  • Components have independent release cycles
  • Different teams own different components
  • Components are reused across unrelated projects
  • You need fine-grained access control

cmod supports both models. Workspace members can depend on external Git repositories, and standalone projects can depend on workspace members via path or Git URL.

Get started with cmod workspaces and structure your C++ monorepo the right way.