title: "Monorepo is not Monolith — and the difference decides your CVE response time"
published: false
description: "Repo layout and runtime topology are orthogonal axes. Here's what each one actually costs you in version control, updates, scaling, and scanning."
tags: architecture, devops, security, kubernetes
cover_image:
canonical_url:
Repo layout and deployment topology are orthogonal. Most orgs conflate them, split their runtime to solve a version-control problem, and end up with a distributed monolith that fails together, deploys together, and now also drops packets.
The decision that actually matters is where you enforce a boundary — compiler, or network — and you make that choice once per boundary, not once per company.
The two axes
| Axis | Question it answers | Reversible? |
|---|---|---|
| Source layout (mono/poly-repo) | How do teams land code? | Yes — painful but mechanical |
| Deployment unit (monolith/services) | What ships and scales independently? |
No — data migrations aren't git revert
|
Why is "polyrepo + monolith" so common if it's strictly worse?
Because it arrives by accretion, not decision. Someone extracts a shared auth library into its own repo for "reuse," and now a one-line change is two PRs, a release, and a version bump — with none of the deploy independence that would justify it. The tell: your internal libraries have semantic versions but only one consumer.
What actually runs
A network call is a function call that acquired five new failure modes and lost its type checker. That's the entire trade:
| You gain | You pay |
|---|---|
| Independent deploy | Timeouts, retries, backoff, idempotency keys |
| Independent scale | mTLS certs + rotation, SPIFFE identity |
| Independent failure | Distributed tracing, or you debug blind |
| Per-workload least privilege | NetworkPolicy per pair, contract versioning |
| Team autonomy | Eventual consistency where you had a transaction |
The availability math nobody runs first
Services in a synchronous request path multiply, they don't average. For services each at availability :
At a respectable per service:
| Services in path | Path availability | Annual downtime |
|---|---|---|
| 1 (monolith) | 99.90% | ~8.8 h |
| 5 | 99.50% | ~43 h |
| 20 | 98.02% | ~174 h |
Any of these = the costs of both architectures, the benefits of neither. Merge them back.Distributed monolith: the smell test
Version control and updates
An engineering drawing has one revision block. A drawing set has a rev per sheet plus an index saying which revs go together. That index is the whole problem with polyrepo microservices.
| Concern | Monolith | Microservices | Monorepo mitigates? |
|---|---|---|---|
| Unit of change | Caller + callee in one commit; the compiler catches the mismatch | Two commits, two repos; nothing catches it until prod — unless you run consumer-driven contract tests | ✅ Atomic commit restores the invariant |
| Breaking a contract | Rename, fix call sites, merge | Expand → migrate → contract: 3 releases, maybe 3 teams | ✅ Partially — still need runtime bake |
| "What was running at 03:14?" | git tag |
Deploy manifest / cluster state. Git cannot tell you. | ❌ Orthogonal — that's GitOps |
| Bisecting a regression |
git bisect, works |
Bisect across the deploy manifest, by hand | ✅ Meaningfully |
| Update blast radius | Full-fleet restart for a one-line fix | One deployment — the real win | ❌ |
| Base-image bump | 1 Dockerfile, 1 PR | N Dockerfiles, N repos, N reviews, N releases | ✅ The killer argument |
| Dependency confusion risk | One lockfile, one registry config | N lockfiles, N registry configs, one misconfigured | ✅ |
Scalability
The only scaling argument that survives contact is divergent resource profiles. If your resizer is CPU-bound and spiky while auth is memory-flat and steady, you're sizing every replica for the resizer and paying for idle auth 24/7. That's a real bill.
If every module has the same profile, splitting buys you a network hop and a Grafana dashboard.
| Concern | Monolith | Microservices |
|---|---|---|
| Horizontal scale | Add replicas behind an LB. Stateless monoliths go much further than folklore suggests. | Per-service HPA/KEDA. Efficient only when profiles diverge. |
| Vertical waste | Pod sized for the hungriest module | Right-size per service |
| Failure isolation | Reporting's leak OOM-kills auth. Shared fate. | Reporting dies alone — iff callers have timeouts and breakers |
| The database | Usually the actual bottleneck | Splitting app code does not split the DB. Real split means outbox and sagas where you had BEGIN TRANSACTION. |
| Team scale | Merge contention past ~20–30 engineers — solvable with a merge queue | Conway's law made explicit: independent cadence per team |
Supply chain and scanning
This is where the two architectures invert, and where most security programs get it backwards.
| Concern | Monolith | Microservices |
|---|---|---|
| SAST | One scan, whole call graph. Taint analysis works end to end. | Per-service scans. The scanner cannot follow taint across a network hop — cross-service injection is structurally invisible. Under-priced regression. |
| SCA | One lockfile, one fix. But one bad transitive dep taints everything. | N lockfiles. Smaller each; drift guaranteed; inventory mandatory. |
| Findings noise | Big image → many findings, most in code paths never executed. Kill this with VEX, not with an architecture change. | Slim images, genuinely fewer findings. Real benefit. |
| Fleet CVE response | Hours. Bump, rebuild, roll. | Days to weeks — unless monorepo or golden-base auto-rebuild. Ask your worst team, not your best one. |
| Runtime attack surface | One identity, one NetworkPolicy. Compromise = full app + full DB creds. | Per-workload SPIFFE identity, per-pair NetworkPolicy, minimal filesystem. The genuine security win. |
| Policy enforcement | One gate | Enforce centrally at admission, never per-repo. Per-repo policy rots to the level of the least-staffed team. |
| Provenance / SLSA | One attestation, one control mapping | N attestations. SLSA L3 needs a hardened build platform — N times, or one shared platform. |
The pattern that actually works, regardless of which side you land on:
golden base image (hardened UBI/distroless)
└─> bump triggers rebuild of ALL dependents
└─> SBOM + in-toto provenance attestation
└─> cosign sign
└─> admission control verifies signature (Kyverno/Gatekeeper)
└─> Dependency-Track fleet inventory
The gate
Split a service out only if you can say yes to at least one trigger and yes to all the mitigations.
Triggers — at least one:
- Materially different resource profile (CPU vs memory, spiky vs flat)
- Materially different failure domain (this must survive when that dies)
- Materially different compliance boundary (this touches PII and that doesn't)
- A team that needs independent cadence and owns the whole domain
- Different scaling ceiling or language/runtime, with a real justification
Mitigations — all of them:
- Callers have timeouts, jittered retries, circuit breakers
- The service owns its data; no shared schema
- Contract tests in CI on both sides
- Distributed tracing before the first split, not after
- Golden base image with rebuild-on-bump
- Centralized admission policy and signed images
Not on the list, and never a valid trigger: "the codebase is big." Big is what modules are for.
Verdict
Start monorepo + modular monolith. Enforce module boundaries in CI (import linting, architecture tests) so the seams are real before they're physical. Split one service at a time, strangler-fig style, only against the gate above. Stay monorepo through the split — it's the cheapest insurance you'll ever buy against base-image drift.
| Axis | Winner | Confidence |
|---|---|---|
| Version control | Monorepo | High — few real counterarguments below ~1000 engineers |
| Updates | Monolith | High |
| Availability | Monolith, unless calls are async | High |
| Scalability | Microservices, iff profiles diverge | Conditional |
| Supply chain | Monorepo + microservices + golden base | High |
Which axis does your org get wrong? I'd guess the one nobody wrote an ADR for.




Top comments (0)