How do you optimize Bitbucket Pipelines for monorepo architectures where multiple services share a single repository?
Quick Answer
Monorepo pipeline optimization uses Bitbucket's changeset conditions to run steps only when specific directories change, parallel steps for independent services, and conditional custom pipelines for targeted deployments. Advanced strategies include generating dynamic pipeline configurations, maintaining per-service caches, and using artifacts to share build outputs between dependent services.
Detailed Answer
Think of a monorepo pipeline like a hospital emergency room triage system. When a patient (commit) arrives, the triage nurse (changeset condition) examines them and routes them to the appropriate specialist (service-specific pipeline steps). A patient with a broken arm does not need a cardiologist, and a patient with chest pain does not need an orthopedic surgeon. Without triage, every patient sees every specialist, wasting everyone's time. In pipeline terms, without changeset conditions, every commit triggers every service's build, wasting build minutes and slowing developer feedback.
Bitbucket Pipelines supports changeset-based conditional execution through the 'condition.changesets.includePaths' directive on pipeline steps. When a step has an includePaths condition, it only runs if the commit modifies files matching the specified glob patterns. For a monorepo with directories like services/payments/, services/orders/, and services/inventory/, you define separate steps for each service with conditions matching their directories. A commit that only changes files in services/payments/ triggers only the payments build step, not the orders or inventory steps. This reduces build time from 'sum of all services' to 'only affected services.'
The pipeline architecture for a monorepo typically follows a two-stage pattern. The first stage runs shared concerns in parallel: linting the entire repo, running shared library tests, and validating infrastructure-as-code. The second stage runs service-specific steps in parallel, each with changeset conditions. This structure ensures shared quality gates are always enforced while service-specific builds only run when relevant. For deployment, each service has its own deployment step with a manual trigger and deployment environment, allowing independent release cadences.
In production, mature monorepo setups face several challenges. Cache management is the first: each service has its own dependency tree, and a single 'node' cache would be invalidated by any service's dependency change. The solution is custom caches per service (e.g., 'payments-npm' caching services/payments/node_modules). Build minutes optimization is the second: Bitbucket's changeset conditions work at the step level, not the pipeline level, so even skipped steps consume a small amount of overhead. Organizations with large monorepos sometimes use a pre-pipeline script that determines affected services and triggers custom pipelines for only those services via the API, avoiding the overhead of many conditional steps. Third, dependency graph awareness: if the shared library in libs/common/ changes, all services that depend on it must rebuild. This requires maintaining a dependency map and expanding the changeset condition's include paths to cover upstream dependencies.
A gotcha that causes missed builds: changeset conditions compare the current commit against the previous commit on the same branch, not against the destination branch. For pull request pipelines, the comparison is between the PR's head commit and the destination branch. This distinction matters because a merge commit on main may include many files across multiple services, triggering all service builds even if the original PR only touched one service. Another trap is the 50-step limit per pipeline in Bitbucket Cloud: a monorepo with 20 services, each needing 3 steps (test, build, deploy), exceeds this limit. Teams work around this by consolidating steps (combining test and build into one step) or using custom pipelines per service triggered via API calls from a lightweight orchestrator pipeline.