Table of contents
- Organize builds across languages
- Manage dependencies and internal packages
- Streamline development workflows
- Smarter CI/CD for multi-language monorepos
- Managing PRs in a multi-language repo with Graphite
- Final thoughts
Monorepos allow teams to manage all their code in one place—even when that code spans multiple programming languages. Whether you’re building a Go backend, a React frontend, and a Python ML service, monorepos promote shared tooling, atomic changes, and a unified view of the codebase. But combining different ecosystems under one roof also introduces complexity. In this post, we’ll cover practices for handling multi-language monorepos effectively, with examples, tools, and a look at how Graphite can simplify PR workflows.
Organize builds across languages
Every language has its own build tool: go build
, tsc
, cargo
, python setup.py
, etc. A monorepo needs a system to manage and orchestrate these builds. Some teams use polyglot build systems like Bazel or Pants, which support multiple languages natively. These tools understand dependencies and can rebuild only what’s necessary, improving speed and efficiency.
For teams that don’t want a heavy build system, Nx and Turborepo offer lighter task runners. While originally focused on JavaScript, they can invoke arbitrary commands—useful if you need to run pytest
, go test
, or cargo check
as part of a pipeline.
Regardless of tooling, modular organization is key. Projects should be grouped logically—e.g., frontend/
, backend/go-api/
, lib/shared/
—so tooling and CI pipelines can target specific areas. Avoid mixing unrelated services in one directory, and be explicit about which folders belong to which team or product.
Manage dependencies and internal packages
Each language brings its own dependency manager: npm
, pip
, go mod
, cargo
, etc. In a monorepo, consistency matters. For projects within the same language, consider a single version policy (SVP) to avoid version drift—e.g., all Node apps use the same React version.
Tools like npm workspaces, Poetry, or Go modules support internal linking, so projects can depend on local packages without publishing them externally. This allows for internal code reuse and atomic updates. For example:
In Node:
Terminal"my-utils": "workspace:*"In Go:
Terminalreplace github.com/org/shared => ../sharedIn Python, use relative paths or install from local wheels.
Automated dependency updates via tools like Renovate or Dependabot help keep things fresh across multiple languages. These tools can scan package.json
, requirements.txt
, go.mod
, etc., and open update PRs automatically.
Streamline development workflows
A multi-language monorepo can overwhelm developers with differing tools, commands, and conventions. Standardizing tasks is critical. Offer unified scripts (e.g., repo test
, repo lint
) that call into the right language-specific tooling. A task runner like just
, Makefile
, or Nx can help unify commands.
Use linters and formatters per language—eslint
, black
, gofmt
, rustfmt
—and enforce their use in CI. Define team-owned areas using CODEOWNERS
so the right reviewers are assigned automatically.
Documentation is essential in polyglot repos: clearly outline how to add a new project, link dependencies, or test a service. For onboarding, consider Docker or environment managers like asdf to simplify the setup across runtimes.
Smarter CI/CD for multi-language monorepos
CI pipelines need to avoid rebuilding the entire repo on every change. Set up jobs to trigger based on path changes—e.g., only run frontend builds if files in frontend/
change. Tools like Nx or Pants can detect affected targets and run jobs accordingly.
Run builds and tests in parallel per language or service. Cache language-specific artifacts (e.g., node_modules
, Python wheels, Rust target/
) to speed up CI. Use remote caches or artifact stores if needed.
For CD, only redeploy the services that changed. You can write simple scripts or use monorepo-aware tooling to compute which services were impacted by a change and deploy just those.
Managing PRs in a multi-language repo with Graphite
PR workflows in a monorepo often involve large, multi-scope changes—e.g., a schema change in Go, followed by updates in Python and TypeScript. Graphite helps break this down with stacked pull requests, where you split a large change into dependent PRs that can be reviewed and merged incrementally.
For example:
- PR 1: Add new API field in Go backend
- PR 2: Update data layer in Python service
- PR 3: Update frontend to use new field
Each PR depends on the previous one, but reviewers can look at them in isolation. Graphite tracks these relationships and lets you manage the stack easily.
Graphite also offers a merge queue, which automatically rebases and tests PRs before merging. This reduces conflicts, flaky test merges, and redundant CI runs—especially helpful in busy repos. Instead of testing every PR in isolation, Graphite can batch them into a single test run.
Other benefits:
- Visual dashboards for stacked diffs
- Slack notifications for review requests
- Automation for labeling and routing based on changed files
These features are especially helpful when you’re coordinating changes across languages, services, or teams in one repo.
Final thoughts
A monorepo with multiple languages is manageable when using clear structure, build orchestration, dependency hygiene, and CI targeting to keep things fast and maintainable. When it comes to collaboration, tools like Graphite help teams stay productive by organizing large changes into smaller, reviewable units and automating away Git complexity.
A well-run multi-language monorepo becomes a single source of truth for your organization’s software—and with the right practices in place, it scales with your team.