Skip to content
Profile
E. Burgos
October 15, 2026By Esteban Burgos8 min read

Microfrontends with Nx, Part 2: How to Split Domains, Version Contracts, and Survive CI in a Real-World Case

#microfrontends#Nx#monorepo#arquitectura frontend#CI/CD
Microfrontends with Nx, Part 2: How to Split Domains, Version Contracts, and Survive CI in a Real-World Case

A while ago I wrote "Microfrontends in the Agentic Era: Orchestration, SDD, and the Beauty of the Monorepo with Nx," where I explained why Nx makes sense for this kind of architecture. In this second article I share the practical side: what those decisions look like when you have to make them with a real team and a real timeline.

I'm not going to repeat the fundamentals from the first one, but I will bring those same concepts into an applied case: how we divided domains into apps and libraries, what contracts we defined between microfrontends, how we versioned them, and what got complicated in CI when the monorepo stopped being a demo project and started behaving like what it actually was: a platform with multiple teams pushing code at the same time.

The underlying problem: dividing by domain, not by screen

The first temptation when you build microfrontends in an Nx monorepo is to divide by route or by screen. "This is the login app," "this is the checkout one," "this is the profile one." It works for the first few weeks, and then it starts to hurt: every new feature ends up touching four different apps because screens don't respect domain boundaries, they respect navigation boundaries.

What worked for us was flipping the question. Instead of asking "what screens do we have?", we asked "which business domains have their own lifecycle, their own team, and their own reason to deploy?" It's the same question you'd ask in a backend built with microservices, and there's no reason not to ask it on the frontend side too.

With that logic, the apps in the monorepo ended up mapping to business domains with a clear owner, not to UI sections. An app isn't "the billing screen," it's "the billing domain, with everything that implies: its screens, its states, its rules." When a screen needs to show information from two domains, that's not solved by merging apps: it's solved with runtime composition or with well-scoped shared libraries, which is what the rest of the monorepo is for.

Apps vs. libraries: the rule that avoided chaos

Once the domains were clear, the question that came up every day was: does this go in an app or in a library? Nx doesn't give you that answer for free, it gives you the structure so the answer can be consistent.

The rule we adopted, and stuck to with discipline even when it was costly at times:

  • Apps are deploy shells. If something is deployed independently, it's an app. If it doesn't deploy on its own, it's not an app, it's a library.
  • There are three types of libraries, and they don't mix. Domain libraries (logic and components specific to a business, consumed only by their domain), shared UI libraries (design system, generic components, with no business logic), and infrastructure libraries (HTTP clients, authentication utilities, cross-cutting configuration).
  • No domain A imports domain B's code directly. If two domains need to share something, that something moves up a level: it becomes a shared library or an explicit contract, never a silent cross-import.

This last rule is the one that required the most linting rules, but also the one that helped us the most. Nx has boundary tools specifically to enforce this—dependency restrictions between projects based on tags—and we used them. An architectural discipline that can be violated without CI flagging it ends up being a wish rather than a rule.

Contracts between microfrontends: the point where everything gets serious

Here the conversation stops being about Nx and starts being about distributed systems architecture, which is, at bottom, what well-built microfrontends actually are.

Each microfrontend exposes a contract: what events it emits, what props or parameters it expects to mount, what data it needs from the shell or from other microfrontends to work. The temptation is to let that contract live implicitly in the code, something "everyone knows" because they're in the same repo. It's a mistake that gets expensive as soon as the team grows or someone new joins.

What we prioritized was treating those contracts the way you'd treat an API contract: with an explicit, versioned schema, and with the same seriousness you'd give a REST contract or an event on a message bus. In practice, this meant:

  • Defining the contract in a visible place in the monorepo, not buried in each microfrontend's implementation. A shared types or schemas library that acts as the source of truth for what each domain expects and delivers.
  • Versioning the contract, not just the code. A change to a microfrontend's contract can break another team that isn't looking at that code every day. We treated those changes with the same caution as a breaking change to a public API: new version, coexistence period, explicit communication.
  • Separating the contract from the implementation. The shell orchestrating the microfrontends should only know the contract, never the internal details of how a domain resolves its logic. That is what lets you deploy microfrontends independently without every deploy being a risk event for the whole system.

This idea of explicit, versioned contracts connects directly with the Specification-Driven Development approach I mentioned in the previous article: the clearer and more executable the specification of what a microfrontend promises, the less room there is for integration to break in production because of an assumption nobody wrote down anywhere.

Where things really got complicated: CI

If the first article talked about the beauty of the monorepo, this one also has to talk about its friction, because leaving it out wouldn't be honest. And the most real friction showed up in CI, not in the code.

When several teams push to different apps and libraries in the same monorepo, a CI pipeline that runs "everything, always" quickly becomes slow and unsustainable. And a slow pipeline isn't just a productivity problem, it's an architecture problem: if feedback takes too long, people start skipping validations, and that's where contracts between microfrontends start breaking without anyone noticing until production.

What specifically got complicated:

  • Running the entire pipeline on every commit stopped being viable as soon as the number of apps and libraries grew. Running tests and builds for domains that weren't even touched by a change is wasted CI time.
  • Detecting what actually broke when a shared library changed became increasingly manual, right when it needed to be more automatic.
  • Coordinating independent deploys with versioned contracts required CI to know not just what to build, but which version combinations were compatible between microfrontends.

The solution came from leaning on something Nx already does very well: the monorepo's dependency graph. Instead of always running everything, the pipeline switched to running only what the graph identifies as affected by a given change. That, combined with caching of already-executed tasks, noticeably reduced feedback time without sacrificing coverage: if a change doesn't touch a domain, that domain doesn't get rebuilt or retested in that pipeline.

The second piece was discipline around the boundaries I mentioned earlier: if CI automatically validates that no one violated the dependency rules between domains, most integration problems get caught before reaching a pull request reviewed by humans, not after.

What I took away

  • Business domains make better boundaries than screens. Asking who owns what, and why it deploys separately, brings more order than dividing by navigation.
  • The app vs. library rule, backed by linting, is worth what it costs. Nx's automatic boundaries turn a convention into something verifiable.
  • Contracts between microfrontends are treated like API contracts: explicit, versioned, with a coexistence period.
  • CI is architecture too. A pipeline based on the affected graph and caching keeps feedback fast, and that protects the contracts.
  • None of this came for free. Dividing by domain means uncomfortable conversations with the business about where the boundaries are, versioning contracts means more ceremony than "just import the component," and optimizing CI is engineering time spent on something nobody outside the team sees. Still, a monorepo without clear boundaries or explicit contracts isn't simpler: it's only simpler at first.

If the first article was about why Nx and well-designed microfrontends make sense in the agentic era, this one is about what that looks like when you sustain it day to day with a team. If you're working on something similar, I'd enjoy exchanging experiences: you can reach me on LinkedIn. And if it's useful, on my portfolio I describe how I work on this kind of project: estebanburgos.com.ar/portfolio.

Written by Esteban Burgos

Comments

Be the first to comment.