When a monorepo slows you down

When a monorepo slows you down

A shared repository. Reusable libraries. Consistent tooling. One place to make a change. On paper, a monorepo can look like the obvious way to help teams move faster. In practice, that small change can leave you waiting for another team's review, another team's priorities and another team's agreement about what the shared component should do.

In my experience, having to coordinate with other teams can make development slower, even when the architecture looks cleaner. That does not mean collaboration is bad or that Nx is the problem. It means we should stop treating shared code as free.

I understand the appeal. I also think we regularly abstract too much, too early, and then call the result simpler because there are fewer copies of the code.

I understand the point

A monorepo keeps multiple projects in one repository. Nx adds tooling that understands relationships between projects and helps manage the work around them. Those are useful capabilities, particularly when applications really do need to evolve together.

There are legitimate benefits: common conventions, changes spanning a library and its consumers in one commit, and a clearer view of dependencies. Nx's task caching and affected-project analysis can also avoid repeating unnecessary work when configured correctly.

I would not dismiss any of that. Fixing one genuine shared rule in one place can be much safer than discovering that five applications have quietly implemented it differently.

One repository is not one release

A monorepo does not inherently require every application to deploy together. Nor does Nx require every team to use one enormous shared component library. Repository layout, dependency boundaries and release policy are separate decisions.

That distinction matters because it is easy to blame the tool for organisational choices made around it. Moving the same tightly coupled design into separate repositories would not automatically restore team autonomy. It might simply add package publishing to the existing coordination problem.

The cost is between the teams

A dependency is more than an import statement. If changing it requires another team's time, it is also a dependency on their availability and priorities.

The other team may be entirely reasonable. They have customers, deadlines and a backlog too. Your urgent feature can be a minor concern for them. A shared repository makes their code accessible; it does not create space in their sprint.

From the perspective of your team, a change that would be straightforward locally now includes a negotiation. That negotiation may be worthwhile. It still belongs in the cost of the architecture.

A small field, a larger discussion

Consider a hypothetical pair of Angular applications sharing a customer form. Both initially collect a name, an email address and an address. Extracting one common component looks sensible.

Then one product needs an address to be optional, a different validation moment and a second contact person. The other product must keep its existing behaviour. Now the request is about the shared API, compatibility, test coverage and who approves the change.

You could add three configuration options. You could add extension points. You could create a wrapper around the wrapper. Each answer may be defensible on its own. Together, they can produce a general-purpose form that is harder to understand than either original form.

The source code was similar. The products were not necessarily making the same decisions.

DRY does not mean share everything

There is a difference between duplicating a business rule and having two pieces of code that happen to look alike today.

If two applications implement the same agreed tax calculation, a shared implementation may prevent expensive disagreement. If two screens both display a title above an input, their similarity is much weaker evidence that they should share an evolving abstraction.

I want to know why the pieces should change together. If there is no convincing answer, removing the duplication can introduce a relationship the product did not need.

When the flags start multiplying

A shared component with options such as useLegacyValidation, isPartnerPortal and hideSecondaryContact deserves a conversation. Those names suggest that knowledge about individual products is moving into the common layer.

Sometimes that is a transitional compromise. Sometimes the abstraction is telling you that it does not represent one coherent thing anymore. Adding another flag without reconsidering the boundary can make every consumer responsible for understanding behaviour it does not use.

A little duplication can be cheaper than a dependency between independently changing features. That is not permission to copy an important rule everywhere. It is permission to compare the actual costs instead of counting repeated lines.

KISS should challenge the abstraction

We invoke DRY and KISS as if they automatically point in the same direction. One implementation looks less repetitive, so surely it must also be simpler.

But simpler for whom?

A local component may be easy for one team to change. A shared component may reduce total code while requiring several teams to understand its configuration, compatibility promises and review process. The diagram has fewer boxes, but the work has more conversations.

KISS should make us question that complexity. It is not a reason to centralise everything. The mistake is treating an abstractly tidy design as proof that the day-to-day development experience will be straightforward.

If a small product change needs three teams to agree, I want to know what value that dependency is buying us.

A faster build cannot approve your change

Nx can help identify affected projects and reuse task results. It cannot decide that another team's compatibility concern is unimportant, or approve a change on their behalf.

That is why I would measure the full journey from a requirement to production. Build duration is one part of it. Time waiting for a decision, review or coordinated release can matter just as much.

Look at the dependency graph

If a change to a common library affects half the workspace, running checks for those consumers may be the correct behaviour. The question is why so many projects depend on that library and whether the dependency is as narrow as it could be.

Affected analysis is not a promise that every pull request will be small. It reflects the graph and configured inputs. A broad shared layer can turn a local intention into a wide set of things that need verification.

Optimise the pipeline, but also inspect the architecture that creates the work. Faster execution is useful. Avoiding an unnecessary dependency may be more useful.

What I would share

I am more comfortable sharing capabilities with a clear contract and a real reason to stay consistent. A focused UI primitive, an agreed API contract or a well-defined business calculation can fit that description.

I am more cautious about sharing complete product workflows just because their first version looks similar. Product-specific validation, navigation and permissions can diverge quickly. Keeping those decisions close to the product team may make both the code and its ownership clearer.

Share the stable part

For the customer-form example, share an accessible input or a small address field group if the behaviour is genuinely common. Let each application compose its own workflow and define its own product rules.

That still provides reuse. It just avoids making the shared package responsible for every difference between the applications.

I would also keep ownership explicit. Who maintains the library? Who can review a contribution? What behaviour is promised to consumers? How do urgent fixes work? “Everyone owns it” can leave a contributor unsure who is actually allowed to say yes.

Make boundaries enforceable

Nx offers module-boundary rules that can restrict dependencies using project tags. That is useful when the team has agreed which projects may depend on which others.

For example, shared UI should not casually import a customer application's internal state. Otherwise, the supposedly reusable layer starts depending on a particular product. A written architectural intention is much easier to preserve when tooling can detect that violation.

But a lint rule cannot tell you whether a business abstraction is sensible. You still need to review the responsibility of the library, the shape of its public API and whether its consumers genuinely benefit from changing together.

Allow a way out

Shared code should not become a permanent obligation simply because somebody extracted it once. If consumers develop different requirements, splitting the abstraction can be a healthy decision.

I would rather have two clear implementations with deliberate ownership than one increasingly configurable framework that every team is afraid to change. Reuse is useful when it removes work. Preserving reuse at any cost can do the opposite.

When the trade-off works

A monorepo can work well when projects share meaningful contracts, contributions across boundaries are practical and teams can make coordinated changes without waiting indefinitely. A maintained common platform can make development more consistent and easier to navigate.

The benefits become less convincing when unrelated products are pushed into shared workflows, ownership is unclear or every small change needs a committee. Those are organisational and architectural problems. Nx can support better boundaries, but installing it does not resolve them.

Before extracting another library, I would ask:

  • Do these consumers share the same rule, or only similar code?
  • Should they change together when their product requirements change?
  • Who can maintain and approve changes to the shared contract?
  • Can each team still release independently where it needs to?
  • What is the escape route if the requirements diverge?
  • Are we reducing delivery time, or just reducing duplicated lines?

My objection is not that teams should avoid working together. Collaboration is essential when the problem actually crosses their boundaries. I object to creating extra boundaries to cross and then presenting all that coordination as an automatic improvement.

I understand the essence of a monorepo, and I understand why Nx exists. I want us to apply the same scrutiny to the human dependencies as we apply to the imports. An architecture that looks efficient on paper should still have to prove that it helps people deliver software.

Comments

There are no comments yet, leave yours below.

Leave a comment

Do you have an addition, question or experience related to this article? Share it below.

Comments are briefly reviewed before they appear.

Read more about:

The bullshit of code coverage

A test suite can reach 100% coverage and still charge the wrong shipping fee or take a payment twice. Through boundary cases, misleading mocks and missing requirements, I examine what coverage actually proves. The useful question is not whether the code ran, but whether the tests would catch the mistakes your customers care about.

Continue reading

Security by design in Angular

Security by design in Angular

A route guard and a green pipeline do not stop one customer reading another customer's invoice. Using Angular and a .NET API, I turn a security requirement into server-side ownership checks and tests. Security by design starts with boundaries and evidence, before choosing a scanner.

Continue reading

Building small apps with Swift

A small native app can solve a recurring annoyance better than another oversized platform. Using our Loop apps as examples, I discuss choosing a narrow problem, working with Swift and SwiftUI, and respecting the differences between Mac and iPhone. Distribution, accessibility and support are part of the product too, even when the feature list is deliberately short.

Continue reading