06 May 2026
7 min

Beyond Clean Code. Building a Scalable Angular Frontend Architecture with Nx Monorepos.

From the moment we start our career in web development, we learn that we have to become great at Clean Code. It sounds like the ultimate goal, right?

But here’s the thing: when you’re building enterprise scale applications, clean code alone isn’t enough, and I will dare to say neither a tidy folder structure.

For long-term maintainability, we need a Scalable Architecture that remains performant as the application grows in complexity and the number of developers working on it .

In this article we are focusing on what’s known as a Modulith — a Modular Monolith. A single deployable frontend application whose internal architecture is structured as micro-services.

What is a Modulith?

The term Modulith comes from combining two words: Modular and Monolith. No no, wait. I know! By reading the “monolith” you think that it’s time to go away. Don’t go, give it some time..

A monolith gets a bad reputation because people associate it with a big, messy codebase where everything depends on everything else. But you know what? That’s not a monolith problem, but  an architectural one. A monolith simply means your application is deployed as a single unit. That’s it!

In a Modulith architecture we have the discipline of a modular system where every module has a clear domain, has explicit boundaries, and we treat the code, the internal libraries as NPM packages. 

Think of it this way:

  • Microservices: separate deployments, high operational overhead, complex inter-service communication
  • Monolith: single deployment, zero structure, circular dependencies, tight coupling, inverted flows, hard to share code
  • Modulith: single deployment, strong internal structure, enforced module boundaries, clear flow, easy to share
visual comparison of software architecture styles: microservices, big ball of mud, and modulith

The Folder Structure Myth

Most developers mistakenly think that having a clean folder structure means having a clean architecture. While an organized structure is absolutely essential, it is not sufficient on its own.

Consider this common Angular project layout:

src/app/
├── core/
│   ├── guards/
│   ├── interceptors/
│   └── services/
├── shared/
│   ├── pipes/
│   ├── utils/
│   └── components/
└── features/
    ├── products/
    │   ├── state/
    │   ├── components/
    │   ├── services/
    │   ├── models/
    │   └── products.component.ts
    └── users/

This separation sounds great, and in theory it is! Each folder has a clear purpose:

  • Core: Application-wide singleton logic — auth guards, HTTP interceptors, startup services.
  • Shared: Reusable UI components, pipes, directives, and utility functions.
  • Features: Business logic organized by domain (Products, Users, etc.).

But here’s the catch: this structure gives no guarantee that dependencies will flow in the right direction.

Angular workshops: Scalable architecture with a GDE

Circular Dependencies

Let’s say you have a service inside the Products feature that accidentally imports something from the Users feature. A few weeks later, a developer working on Users introduces an import back into Products. We have just introduced a Circular Dependency. A closed loop where Feature A depends on Feature B, which depends right back on Feature A.

This is not just a code smell. It’s a structural failure. Tightly coupled features cannot be changed or tested independently. Any change in one risks breaking the other, and sooner or later the system will become overly complex and hard to maintain. 

circular dependency in Angular, from feature A to feature B, visual representation

Clean Code Has Limits

The thing is, clean code is often too localized. You might have a beautifully written function (and you should) or a perfectly structured component (and you should) that looks clean in isolation, but It neither contributes to, nor prevents the system from becoming chaotic without a proper structure.

In large-scale applications, the challenge shifts from „how do I write a good function?” to „where does this function belong?” Without enforced architectural boundaries, even developers with the best intentions can accidentally create systemic failures. And when that happens, large-scale refactoring becomes a nightmare.

The Code Review Problem

Let’s be honest,  we all know the drill. The clock is ticking, the deadline is approaching, and under that pressure, the code-reviewer might be tempted to compromise code quality to meet the deadline. 

Reviewers focus on the visible stuff — clean code, potential bugs, a quick check for memory leaks. But what about the crucial architectural flaws? Things like circular dependencies or an inverted dependency flow?

These complex structural problems are overlooked because we (the human devs) are not optimized to track systemic dependencies across a massive monorepo. And this leads us to this realization:

„If a rule can be automated, it must be automated.”

Manually fixing things that can be automated are a huge drain on developer time and reviewer cognitive load. Automation tools are great at that and handle them instantly and consistently. By saving this time the reviewer will be more focused on what it truly matters, the business-critical decisions, potential bugs or memory leaks, etc

Nx Boundaries: From Suggestions to Enforcement

Nx isn’t just some fancy tool for speeding up your builds. At its core, it’s a powerful mechanism for architectural enforcement. We’ve all faced that — telling the team during a meeting „please don’t import a feature into a core service” — only to find out months later that this has happened and needs to be refactored.

Instead of us telling the team what the boundaries are, Nx is enforcing that and is also complaining if the rules are not met. 

Tags and Lint Rules

The way it works is elegant. You tag your libraries, like `type:feature` or `type:ui`, `scope:products` or `scope:shared`,  and then define strict boundary rules around those tags. You can literally configure a rule that says:

„Libraries with the tag 'scope:products’ can import from 'scope:shared’. Never the opposite.”

Here’s what that looks like in practice:

// project.json
{
  "name": "products-feature",
  "tags": ["scope:products", "type:feature"]
}
// .eslintrc.json
{
  "@nx/enforce-module-boundaries": ["error", {
    "depConstraints": [
      {
        "sourceTag": "scope:products",
        "onlyDependOnLibsWithTags": ["scope:products", "scope:shared"]
      }
    ]
  }]
}

If a developer tries to cross that line, the linter will immediately flag the violation. Having this rule frees up time for the code reviewer and makes the system more robust and consistent.

Preventing Inverted Dependency Flows

It’s vital in a clean architecture to have clear communication flows between different application layers. Think about it. Should a presentational component ever know about a heavy-duty smart component? Should a core service know about a feature that encapsulates the business rules? Absolutely not. By tagging feature libraries as `type:feature` and UI libraries as `type:ui`, we make it impossible to accidentally import a feature into a UI library:

// type:ui can never import from type:feature
// The linter will catch it immediately — every time

This is achieved either by setting up pre-commit hooks in your IDE or as a final check in CI. The system ensures that problematic code will never get to your main branch.

Organizing Libraries by Type and Scope

In an Angular Modulith, Nx works best when your libraries follow a consistent structure organized by scope and type. Every library has a clear, single responsibility. 

There are four standard library types, and once you understand them, you’ll wonder how you ever lived without this structure.

data-access

This is where all state management and API communication lives. NGXS state slices, selectors, HTTP service calls — all of them belong there. The idea here is that nothing outside the data-access layer should know how the data are fetched or mutated.

// libs/products/data-access/src/lib/products.state.ts
export interface ProductsStateModel {
  products: Product[];
  loading: boolean;
}


@State<ProductsStateModel>({
  name: 'products',
  defaults: { products: [], loading: false }
})
@Injectable()
export class ProductsState {
  @Action(LoadProducts)
  loadProducts(ctx: StateContext<ProductsStateModel>) {
    ctx.patchState({ loading: true });
    return this.productsService.getAll().pipe(
      tap(products => ctx.patchState({ products, loading: false }))
    );
  }
}

ui

It has only pure, presentational components. These components receive data via `input()` and communicate back via `output()`. Nothing more like a usual presentational component. They have zero knowledge of application state, routing, or business logic. Having them in a UI library makes all of them trivial to reuse.

// libs/products/ui/src/lib/product-card.component.ts
@Component({
  selector: 'app-product-card',
  // ...
})
export class ProductCardComponent {
  product = input.required<Product>();
  addToCart = output<Product>();
}

feature

The feature type libraries represent a vertical of your application. They own a business domain, orchestrate data and UI, inject data-access state slices and handle user interactions and flows. They are the entry points of your routed pages

// libs/products/feature/src/lib/product-list.component.ts
@Component({ selector: 'app-product-list', ... })
export class ProductListComponent {
  readonly state = inject(ProductsState);
}

utility

Any stateless pure function lives here. It’s the place for your data formatters, validators, custom RxJS operators. It is your toolbox where you will re-use very often

The result of a clean architecture based on these layers is a dependency graph that flows in one direction. Never the reverse:

feature  →  data-access
feature  →  ui
feature  →  utility
ui       →  utility

Having the Nx boundaries enforcing these rules automatically, this graph can never be compromised — regardless of team size, project age, or deadline pressure.

The ROI of Quality and Developer Experience

But wait, there is a common objection: „Setting up all this structure takes time. We need to ship. We need to ship fast!” And yes, it does take upfront time! But good architecture is not a luxury,  it’s a decision that becomes more and more valuable as the project matures.

Let me show you what I mean.

Nx Computation Caching

Nx caches the output of every task — builds, tests, linting — locally and (optionally) in a shared distributed cache.   If the newly added changes do not affect a library, Nx pulls the cached result in milliseconds instead of re-executing the task.

nx run-many --target=test --all
# Only re-runs tests for affected libraries
# Everything else? Cache hit ✓  — instant.

On a large monorepo, this can shrink CI time from 20 minutes to under 2. That’s a huge win!

Reduced Cognitive Load

Should this file go in shared or core? Is this a UI or a feature?  When every library has a clearly defined type and scope, developers always know exactly where to look and where to add new code. 

Furthermore, when a new team member joins the team, they don’t need to spend multiple hours to understand the structure. It’s self documented. The library name reveals the scope, the type reveals the responsibility. That’s a win for onboarding.

Multiple Teams, Zero Conflicts

Since the library boundaries are set, multiple teams can work in the same monorepo without stepping on each other’s toes. Team A owns `scope:products`. Team B owns `scope:orders`. Their work is isolated by design.

To conclude on the ROI: the real cost is not the time it takes to set up the structure. It’s the time you keep spending without it. Slower onboarding, longer CI pipelines, code reviews that miss what actually matters. That cost adds up every single sprint.

Angular workshops: Scalable architecture with a GDE

Conclusion

Being a Senior Developer or Architect means thinking about the system as a whole, not just the function in front of you.

We started this article talking about Clean Code. And don’t get me wrong, clean code still matters. But it’s a local concern. It won’t save you from circular dependencies, inverted flows, or a codebase that no team can navigate after 2 years of growth.

The Modulith approach we walked through,  Nx boundaries, library types, enforced dependency flows, is what makes an application that scales across five teams for the next three years.

Thank you for reading!! 🙂

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.