Blog · 2026-06-10 · ~12 min read
The monorepo decision log: how teams with shared infrastructure document decisions that span multiple services
A senior engineer joins a company that runs six services from a single repository. First week, she reads the root README, the Turborepo configuration, the CI pipeline definition. She can see how the system is built. She cannot see why the six services are structured the way they are — why the user service and the auth service share a library for session validation but the billing service has its own implementation, why the API gateway routes certain request types directly to three services rather than through a single entry point, why the platform team chose pnpm workspaces over Nx workspaces last year. The code for all six services is in the same history. The reasoning for how they relate to each other is not in any of it.
TL;DR
A monorepo collapses the team-boundary signal that polyrepos provide for free. In a polyrepo, the repository boundary signals which team owns which decision: decisions in the auth service repo belong to the auth team. In a monorepo, the service boundary is organizational rather than structural — it's in the org chart, not the file system — and decisions about cross-service concerns can be made by any team whose code touches the shared code. This creates three categories of monorepo decision that need different scoping, governance, and folder placement: service-local (one service team, one service's decisions/ folder), cross-service (two or more peer teams, cross-team ADR with Downstream Stakeholders field), and platform-wide (platform team, root decisions/ folder, all service teams downstream). The monorepo's unified commit history provides one advantage over polyrepos for decision recovery: git log across the entire codebase narrows the extraction window for AI chat sessions, making the WhyChose extractor more precise when reconstructing historical decisions.
Why polyrepos make decision scoping easy and monorepos don't
In a polyrepo, team-boundary information is implicit in the repository structure. The auth team owns the auth repo. Decisions about authentication, session handling, token formats, and user identity belong in the auth repo's decisions/ folder. When a new engineer joins the auth team and asks "why do we use RS256 instead of HS256 for JWT signing?", the answer should be in the auth repo's decisions/ folder, not in the API gateway repo or the user service repo. The repo boundary is the first-order answer to "whose decision is this?"
A monorepo removes that structural signal. All six services' code is in the same repository. The auth service, the user service, and the API gateway all live in /services/. The session validation library they share lives in /libs/shared/. A decision about session validation may have been made by the auth team, the user service team, the platform team, or informally by whichever engineer happened to write the shared library. The fact that the code is in /libs/shared/ tells you where the implementation landed. It tells you nothing about who made the decision, when, or why that particular boundary was drawn rather than a different one.
This is the monorepo attribution problem: the unified codebase obscures the decision boundaries that team structure creates. A new engineer looking at the session validation library can read the code. They cannot read the decision from the code — whether the auth team proposed it, whether the user team objected to a different approach, or whether "shared library" was the platform team's mandate or an emergent outcome of two teams writing similar code and one engineer suggesting they combine it.
The problem compounds over time. In year one of a monorepo, the team that made each significant decision remembers making it. In year two, the team has turned over, the library has been extended by three engineers who weren't there for the original decision, and the fact that a decision was made at all is no longer obvious. The code exists. The reasoning for the code's structure has vanished.
Three categories of monorepo decision
Before deciding where an ADR goes, monorepo teams need to categorize the decision by its downstream impact. The three categories correspond to three different scopes, governance approaches, and folder locations.
Service-local decisions
A decision that affects only one service and is fully reversible by that service's team without requiring changes to any other service's code. Examples: the auth service switching from in-process session cache to Redis; the billing service adopting a new test framework; the user service adding a feature flag system. These decisions belong in the service team's own decisions/ folder and need no cross-team governance. The service team writes the ADR, reviews it internally, merges it. No announcement is needed beyond the service team's own channels.
Service-local decisions are the majority of ADRs in any system — 70–80% in a healthy decision log. The key diagnostic question: if this decision were reversed tomorrow, which teams would need to change code? If the answer is "only ours," it's service-local. If the answer includes another team's name, it's cross-service or platform-wide.
Cross-service decisions
A decision that affects two or more service teams, requires coordination, and creates obligations for teams that don't directly own the relevant code. Examples: the API gateway team and the auth service team jointly deciding that all services must include a standard request-ID header in every outbound request; the user service and billing service teams agreeing on a shared event format for the user-created lifecycle event; two service teams agreeing on which service owns session validation when both currently have local implementations.
These are the cross-team ADRs with downstream stakeholders explicitly named. The governance ceremony matters here: the decision should be documented as a proposal before it's implemented, reviewed by all downstream teams, and the ADR should carry a Downstream Stakeholders field that names which teams accepted which migration obligations. An ADR that says "all services should adopt the standard request-ID header" without naming which teams were consulted and which accepted the obligation is not a complete cross-service ADR — it's one team's announcement of a preference.
Platform-wide decisions
A decision made by the platform team (or equivalent) that affects all services by changing the shared infrastructure, tooling, or build system. Examples: adopting Turborepo for build orchestration; switching the shared observability library from OpenTelemetry Jaeger exporter to Tempo; upgrading the Node.js version across the entire monorepo; changing the CI pipeline to require green status on all services before any service can merge.
Platform-wide decisions are distinct from cross-service decisions in two important ways. First, the authority relationship is not bilateral: the platform team isn't negotiating with service teams, it's making decisions that service teams adapt to. The review window exists for service teams to identify specific integration breakages, not to grant or withhold approval. Second, the notification obligation is org-wide rather than targeted: all service teams are downstream, not just the ones who raised a concern during review.
These decisions belong in the root /doc/decisions/ folder, owned by the platform team, with a longer public comment window than service-local ADRs (typically ten days for platform-wide decisions vs three to five for cross-service, vs zero for service-local).
The platform team's documentation debt
The platform team's decisions are the most consequential and the least documented in most monorepo organizations. There are three reasons this gap is structural rather than incidental.
Platform decisions happen at a different pace than service decisions. Service teams make decisions frequently — feature design, library choice, test strategy, API contract. Platform teams make decisions infrequently but at high blast radius: the Turborepo adoption, the shared logging format, the secret management approach. The infrequency means there's never a decision-documentation habit to build from — each platform decision is novel enough that the team re-evaluates whether to document it. The answer is usually yes, but the delay means the reasoning is never written at the moment of highest clarity.
Platform decisions are often made by individuals, not teams. The platform engineer who evaluates Turborepo vs Nx for a week, runs the benchmark, and recommends adoption has the full deliberation in their head — and often in their AI chat export. The team meeting that ratifies the recommendation may be twenty minutes of show-and-tell rather than deliberation. The ADR should capture the week of evaluation, not the twenty-minute ratification. But the platform engineer who did the evaluation rarely writes the ADR at the moment of evaluation; they write the implementation first and the documentation never.
Platform decisions are invisible until they break. A service-local decision to switch from Jest to Vitest is visible to the service team immediately — the tests either pass or they don't. A platform decision to change the CI pipeline affects every service team, but the effect may not be visible until a specific edge case surfaces weeks later ("why does the pipeline fail when only the auth service is touched but the billing service status is checked?"). By the time the downstream service team surfaces the question, the platform engineer who made the decision has moved on to the next project and the decision rationale exists only in an AI chat session from three months ago.
The WhyChose extractor is particularly valuable for the platform engineer's scenario: the week of evaluation before a major platform decision is exactly the session type the extractor is tuned for — explicit comparison ("Turborepo caches across services while Nx requires manual graph annotation"), constraint framing ("given that our CI budget is $X per month"), and decision language ("we're going to use Turborepo because"). Exporting that week's ChatGPT history or Claude conversations produces a recoverable record even if the platform engineer forgot to write the ADR at the time.
The monorepo ADR folder structure
A well-structured monorepo decisions/ layout separates the three decision categories structurally, so the folder location communicates the decision scope without requiring the reader to read the ADR to determine whether it's relevant to their service.
/
├── doc/
│ └── decisions/ # Platform-wide: owned by platform team
│ ├── 0001-monorepo-over-polyrepo.md
│ ├── 0002-turborepo-build-orchestration.md
│ └── 0003-opentelemetry-observability-standard.md
├── services/
│ ├── auth/
│ │ └── doc/decisions/ # Auth service-local: owned by auth team
│ │ ├── 0001-rs256-jwt-signing.md
│ │ └── 0002-redis-session-cache.md
│ ├── user/
│ │ └── doc/decisions/ # User service-local: owned by user team
│ └── billing/
│ └── doc/decisions/ # Billing service-local: owned by billing team
└── libs/
└── shared/
└── doc/decisions/ # Shared library: cross-service by definition
├── 0001-session-validation-shared-library.md
└── 0002-request-id-header-format.md
The /libs/shared/doc/decisions/ folder is the location with the highest documentation priority in a monorepo. Every decision recorded there is cross-service by definition — the shared library only exists because multiple services depend on it, so every decision about its design, its interface contract, and its ownership affects multiple teams. A shared library with no decisions/ folder is a high-probability source of future "why was this built this way?" questions that nobody can answer.
The root /doc/decisions/ folder gets numbered sequentially from 0001, separate from each service's sequence. This means ADR 0002 might exist three times: once in the root (the Turborepo decision), once in the auth service decisions/ (a different second decision for the auth team), and once in the shared library decisions/ (a third decision about the shared library's second design choice). The numbers are local identifiers, not global ones. If you need global identifiers — for cross-referencing between service and platform ADRs — use the full path as the identifier: doc/decisions/0002-turborepo.md, not just 0002.
How the unified commit history helps decision recovery
A monorepo's unified commit history is, counterintuitively, an advantage for recovering historical decisions. In a polyrepo, tracing when a decision was implemented requires searching across multiple repositories and correlating commit timestamps. In a monorepo, a single git command returns every commit across every service that touched a relevant code pattern.
Practical application for the quarterly extraction pass:
# Find every commit that touched session validation in any service
git log --all --oneline -S "session_validate" --source --remotes
# Find when the shared library gained the request-ID header logic
git log --all --oneline --diff-filter=A -- "libs/shared/src/request-id*"
# Show which engineers touched cross-service concerns in Q1 2025
git log --since="2025-01-01" --until="2025-04-01" --all --format="%ae %s" | grep -i "shared\|libs\|cross"
The commit timestamps narrow the extraction window. If the session validation library was created in a commit on March 14, 2024, the deliberation that preceded it likely happened in February or March 2024. When the engineer who created it runs the WhyChose extractor on their AI chat history, they can filter for sessions from that two-month window rather than searching their entire export history. The precision improvement is significant: two months of sessions produces fewer candidates with more relevant ones than two years of sessions.
The git blame command adds the engineer attribution that service-level commit graphs obscure. git blame -C libs/shared/src/session-validate.ts will show which engineer wrote each line of the shared library — even if it was originally in a service-local file and was later moved to the shared library via a refactor. The -C flag follows content moves across files, which is common in monorepos where code migrates from service-local to shared-library as the pattern becomes more general. The engineer attributed in the git blame output is the engineer whose AI chat export is most likely to contain the relevant deliberation.
This commit-to-engineer attribution makes the onboarding reading list construction more precise for monorepo teams: the platform-wide decisions in /doc/decisions/ are the first ten ADRs every new engineer should read, because they explain the constraints every service operates under. The service-local decisions are relevant only to the engineer's specific service team. The shared library decisions are relevant to every engineer who writes code that calls the shared library — which in a monorepo is often everyone.
The decision that predates the code
The hardest decision category in a monorepo decision log is the one that predates the code it governs. In a polyrepo, this rarely occurs at scale — decisions tend to land in the repo that already exists, so the repo history is the decision history. In a monorepo, code migrates between locations. The auth service that started as a monolith became a service when the monorepo was created. The session validation that was in the monolith became a shared library when the user service needed it too. The decision to move it to a shared library may have been made before the user service was created, when the monolith's lead engineer was designing the anticipated service boundary.
Three patterns help recover these pre-code decisions:
The migration commit as a documentary artifact. The commit that extracted code from one location to another contains the commit message from the engineer who made the decision. If that commit message says "extract session validation to shared library for upcoming user service" rather than the more common "refactor: move session-validate to libs/shared," the decision rationale is in the commit. Mandate meaningful extraction commit messages at the time of the extraction, not retrospectively.
The RFC document that preceded the migration. For significant code migrations in a monorepo — moving service-local code to shared libraries, splitting a service into two, merging two services — the async RFC pattern produces a proposal document that predates the code change and explains the rationale. This is the single most valuable investment a monorepo team can make: requiring an RFC document for any change that moves code between service boundaries. The RFC document lives in the decisions/ folder as a Proposed ADR, and when the migration completes, the Status field updates to Accepted. The decision record and the implementation history are now in sync.
The AI chat extraction pass at migration time. The engineer proposing the migration has the full deliberation in their AI chat from the week they designed the proposal. The WhyChose extractor run at migration time — when the PR is open and the code is in review — produces a recovery artifact that would be difficult to reconstruct six months later. This is the Nygard ADR's Context section in raw form: the constraint that made the migration necessary, the alternatives that were considered (keep it service-local, build a new shared service, use an external library), and the trade-offs that drove the decision.
The platform team's ADR ownership question
Platform-wide decisions create an ownership question that service-local decisions don't: who writes the ADR? In a service team, the engineer who proposes the decision typically writes the ADR. On a platform team making a decision that affects eight service teams, the proposing engineer is often not the person with the most context on the service-team impact — they're the person with the most context on the platform side of the trade-off.
The most effective pattern for platform ADR ownership assigns a named author (the platform engineer who did the evaluation) and a named reviewer from each affected service team (one service team engineer who validates that the service-team impact section is accurate). The service team reviewer doesn't write the ADR; they review the Consequences section to confirm it accurately reflects the migration burden for downstream teams. This is the downstream acknowledgement record pattern applied to platform decisions: the service team reviewer's sign-off on the Consequences section makes the "service teams were consulted" claim auditable rather than asserted.
For the GitHub PR workflow, this means the platform ADR PR has CODEOWNERS entries that auto-request review from a designated representative from each service team. The ADR doesn't merge until those reviews are complete — not to grant veto power to service teams (the platform team retains authority on platform decisions), but to create an audit trail that the service teams were informed and had an opportunity to identify integration issues before the decision was final.
The monorepo AI chat extraction advantage
Monorepo teams have one structural advantage in AI chat extraction that polyrepo teams lack: because all code is co-located, engineers from different service teams regularly open and read each other's code. An auth team engineer opening a PR that touches /libs/shared/ will see the user service's code that depends on it. A billing service engineer investigating a bug will read through the auth service's session validation logic to trace the call path. This cross-service code visibility creates cross-service AI chat deliberation that polyrepo teams don't produce.
In a polyrepo, the auth team engineer rarely reads the user service's code unless there's an explicit integration incident. In a monorepo, the auth team engineer reads it routinely — and when they have a question about how the user service depends on the session validation library, they ask Claude or ChatGPT. That session contains deliberation about cross-service design constraints that belongs in the shared library's decisions/ folder.
Practical implication for the quarterly extraction pass: in a monorepo, the extraction should include engineers from multiple service teams, not just the engineer who owns the relevant code. The user service engineer's AI chat from the month before the session validation library was extracted may contain the key constraint that drove the extraction decision — because the user service engineer was the one who needed the shared library first and discussed the boundary design with their AI assistant before raising it in the RFC.
The multi-engineer pooling approach from the distributed team extraction pass applies with equal force here: three engineers' exports cover the full decision deliberation better than one engineer's export, even when the three engineers are co-located and the decision was made synchronously. The pre-meeting individual deliberation in AI chat often contains the alternatives that never made it into the meeting discussion — the options the engineer evaluated and rejected before proposing the approach they actually recommended.
The monorepo migration decision itself
One ADR that almost no engineering team has written is the decision to adopt a monorepo in the first place. This is the highest-impact organizational and architectural decision the team makes — it shapes every subsequent decision about service boundaries, shared code, deployment coupling, CI investment, and tooling — and it is almost universally made in AI chat, not in a written RFC, because it happens before the team has an ADR practice.
The monorepo migration ADR is a retrospective ADR in almost every case. By the time a team thinks to write it, the migration has been complete for a year or more. The retrospective-confidence field is low or medium: the alternatives (stay polyrepo, adopt a meta-framework over polyrepo, use a hybrid approach with selective mirroring) were likely considered in some form, but the specific trade-offs that drove the decision — "we chose monorepo because the overhead of keeping shared library versions in sync across eight repos was consuming two days per sprint" — may live only in the memory of the two engineers who made the call and in their AI chat exports from 2024.
The monorepo migration ADR belongs in the root /doc/decisions/ folder as 0001. It is the foundational decision that makes all subsequent platform-wide decisions intelligible. Without it, a new platform engineer joining the team has no written record of why the monorepo structure was chosen over the alternatives, which service boundaries were intentional vs emergent, and which aspects of the current structure were explicitly trade-offs rather than oversights.
If your monorepo migration ADR doesn't exist yet: the WhyChose extractor run on the AI chat exports of the two or three engineers who made the migration call is the fastest path to recovering the deliberation. The constraint framing ("given that we're adding three services per quarter and the shared library sync is costing X"), the alternatives considered ("we evaluated Nx workspaces, Turborepo, and a manual workspace setup"), and the decision language ("we chose Turborepo because") will surface from sessions that are likely two to three years old. The context will be incomplete — retrospective-confidence: low — but an incomplete ADR is better than none for the decision that everything else depends on.
Where to start
For a monorepo team that doesn't have a decision log today, the highest-leverage starting point is the shared library documentation gap, not the service-local one. Service-local decisions are recoverable through the service team's memory; shared library decisions are often unrecoverable without the original engineer's AI chat export.
- List every directory in /libs/shared/. Each shared module is a decision. Someone decided it should be shared rather than duplicated in each service. Someone decided what its public interface should be. Someone decided when it was extracted from a service-local implementation vs. written from scratch. These decisions are the foundation of the monorepo's service boundary design, and they should be the first target for extraction and documentation.
- Run the extractor on the AI chat history of the engineers who created each shared module. The git blame on each shared module's initial commit gives you the engineer and the date. That engineer's AI chat from the two months before the initial commit is the extraction target. Request the export, run the WhyChose extractor, and triage the output for sessions that contain the shared module's relevant keywords (the module name, the alternatives it replaced, the service names that depend on it).
- Create the /doc/decisions/ folder and write the platform-wide decisions retrospectively. The monorepo migration decision first (0001), then the build orchestration decision (0002), then the shared observability standard (0003). These three ADRs, written as retrospective records with honest retrospective-confidence fields, give every new engineer the context they need to understand why the platform is structured as it is.
- Adopt the RFC-before-PR requirement for shared library changes going forward. Any PR that adds a new file to /libs/shared/ or changes an existing module's public interface requires an RFC document as a prerequisite. The RFC is short — one page describing the proposed change, the alternatives, and the service teams affected. The PR body links to the RFC. When the PR merges, the RFC's Status field updates to Accepted and it becomes the ADR. This one requirement, applied consistently, keeps the shared library decision log current without requiring a retrospective catch-up every quarter.
- Set up CODEOWNERS to auto-request platform team review on /doc/decisions/. The root decisions/ folder should require platform team review on every PR. This is the governance mechanism that ensures platform-wide decisions aren't made ad-hoc by individual service teams modifying the shared infrastructure without the platform team's awareness. The CODEOWNERS file entry is two lines. The governance it provides is significant.
The monorepo decision log, once it exists, pays off in two places that are particularly acute for monorepo teams: onboarding (a new engineer reading the root decisions/ folder and the shared library decisions/ folder has the full platform picture before they read a single line of service code) and cross-team alignment (when the billing service team wants to modify the shared session validation library, the existing ADR in /libs/shared/doc/decisions/ tells them what the design constraints are and which service teams would be affected by an interface change). These two benefits scale with team size — the value of the decision log grows faster than the cost of maintaining it as the team grows.
If you haven't started yet: join the waitlist and we'll help you run the first extraction pass against your shared library history when it's your turn.
Frequently asked questions
How do you scope ADRs in a monorepo — one ADR folder at the root or one per service?
Both. A monorepo needs three levels of ADR folder: a root-level /doc/decisions/ for platform-wide decisions (infrastructure, shared library choices, monorepo tooling, CI/CD design); per-service /services/[name]/doc/decisions/ for decisions that affect only that service's team; and /libs/shared/doc/decisions/ for shared library decisions (cross-service by definition). The service-local folder is the right default for the majority of decisions. The root folder is reserved for decisions whose downstream impact reaches multiple service teams. The platform team owns the root folder; individual service teams own their own.
What makes the platform team's decisions different from a regular cross-team ADR?
Three properties: scope is org-wide rather than bilateral (every service team is downstream); authority is often unilateral rather than negotiated (platform makes infrastructure choices without service team sign-off on every dependency); and notification flows one-way (platform-to-service, not mutual). This means the governance ceremony is different: a cross-team ADR between peer services should have bilateral review before the decision closes; a platform-wide ADR should have a public comment window for service teams to identify integration breakages, but doesn't require unanimous consent from all service teams.
How does the monorepo's unified commit history help with AI chat extraction for decision recovery?
The unified commit history provides a datable reference point that polyrepos lack. git log --all -S 'session-validation' returns every commit across every service in a single query, letting you narrow the extraction window to the two months before the relevant commit. Narrowing the extraction window reduces noise and increases the precision of the WhyChose extractor's output, because you can focus on sessions where the specific trade-off language appeared rather than searching across an entire multi-year export history.
How do you handle the decision to adopt a monorepo in the first place?
As a retrospective ADR in the root /doc/decisions/ folder as 0001. The deliberation is almost certainly in the AI chat exports of the two or three engineers who evaluated the options — run the extractor on their exports from the evaluation period to recover the constraint framing, alternatives considered, and decision rationale. Use retrospective-confidence: low if the original comparison is only partially recoverable. An incomplete retrospective ADR for the foundational decision is better than none: it tells every subsequent engineer that the decision was deliberate, not accidental, and documents whatever reasoning is recoverable even if the full comparison is lost.