The Limitations of Conventional Tutorials

After a couple of years of Angular development, you might feel that you've mastered the framework. You split your work into components and services, and you might even believe you're following the official style guide. But is that really enough?

Not quite.

What's the missing piece?

That's the right question to ask. Let's explore what it takes to build a well-architected component structure and introduce the concept of lean Angular components.

What Tutorials Don't Tell You

Many developers pick up Angular through resources like the Tour of Heroes tutorial or the Getting Started guide that arrived with Angular version 8.

These resources are perfectly adequate for someone just starting out, but they don't cover how to structure and design large, complex applications. If you assume they contain everything you'll ever need, you're mistaken.

That's precisely the gap we need to fill. Too many learning materials only scratch the surface. Without moving beyond the fundamentals, how can we expect to grow as developers?

Component-based architectures have been central to major frameworks for about five years, starting with React in 2013. But what do we actually mean by "components"? According to the Angular team:

An application is a collection of components.

That definition is rather vague. In fact, calling an application a "composition" of components is more accurate—composition implies a dynamic tree of activated components that exists at any given moment, reflecting what's displayed on the current page.

Components Belong in the Presentation Layer

Components serve the presentational tier of your application—they handle user interaction and display information. They're situated at the outermost layer of the system's architecture, acting as the interface between users and the application's inner workings.

Figure 1. Horizontal layers of a web application. Open in new tab.

Looking at the horizontal layers in Figure 1, components frequently end up handling responsibilities from many of those layers at once. That's a common pitfall.

Model-View-Presenter: A Toolkit of Approaches

I've previously gathered common techniques and detailed them in "Model-View-Presenter with Angular" and its companion pieces. The purpose? To offer a thorough guide on separating responsibilities within an Angular application.

Following my Angular adaptation of Model-View-Presenter will likely set you up with an application that's maintainable, testable, scalable, and fast.

Still not convinced? Fair enough. Let's examine a case from the official Getting Started guide.

// cart.component.ts
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';

import { CartService } from '../cart.service';

@Component({
  selector: 'app-cart',
  templateUrl: './cart.component.html',
  styleUrls: ['./cart.component.css']
})
export class CartComponent {
  items;
  checkoutForm;

  constructor(
    private cartService: CartService,
    private formBuilder: FormBuilder,
  ) {
    this.items = this.cartService.getItems();

    this.checkoutForm = this.formBuilder.group({
      name: '',
      address: ''
    });
  }

  onSubmit(customerData) {
    // Process checkout data here
    console.warn('Your order has been submitted', customerData);

    this.items = this.cartService.clearCart();
    this.checkoutForm.reset();
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 1. Forms: Cart component.

What does the cart component in Listing 1 do? Its UI properties contain a list of items and a checkout form. The items get initialized from the cart service, while the form builder sets up the checkout form.

On form submission, the component logs the form value to the console (a simple illustration), empties the item list via the cart service, and resets the form.

The Problem of Sprawling Responsibilities

Where does this component go wrong? It handles tasks from several horizontal layers at once. It also mixes logic at different abstraction levels, combining low-level implementation specifics with high-level rules.

This component manages two distinct pieces of application state: the checkout form—which is local UI state—and the items in the shopping cart.

State Management Deserves Its Own Place

There are at least two ways this component mishandles application state. The cart items are pulled from the cart service, which at least avoids placing HTTP calls directly in the component. That's a step up in abstraction—we're requesting cart items, not dictating how to fetch them.

However, we're still reaching into the cart service ourselves. For true separation of concerns, the cart component should be purely presentational—responsible only for rendering the cart items and reacting to checkout form submissions. Nothing more, nothing less.

Why does this distinction matter? The official architecture guide explains:

Angular distinguishes components from services to increase modularity and reusability. By separating a component's view-related functionality from other kinds of processing, you can make your component classes lean and efficient.

That's exactly the point. We should aim for components that are devoted solely to presentation. The guide talks about view-related functionality, and while there's room for interpretation, my reading is that it means user interaction and display.

Let's go even further: not all presentation or interaction logic should live inside the component's class. Anything beyond trivial logic ought to be delegated to services and other dependencies.

Crafting Presentational Components

Presentational components accept application state via input properties and display it. When data needs transformation or computed properties, that state is routed through a presenter, which is a service scoped to the component.

The architecture guide elaborates on this in its next passage:

Ideally, a component's job is to enable the user experience and nothing more. A component should present properties and methods for data binding, in order to mediate between the view (rendered by the template) and the application logic (which often includes some notion of a model).

Steering Control Flow

A presentational component also has the job of forwarding user-initiated control flows to services that encapsulate behavior—this is what I call presenters. Any side effects they produce are surfaced through output properties when necessary. In straightforward cases, a user action is directly tied to an output property.

The architecture guide outlines this principle as well:

A component can delegate certain tasks to services, such as fetching data from the server, validating user input, or logging directly to the console.

The three examples listed here align closely with what the cart component does. They serve as clear directives to hand off work to services.

Discipline Is Required

The final paragraph of the guide's introduction says:

Angular doesn't enforce these principles. Angular does help you follow these principles by making it easy to factor your application logic into services and make those services available to components through dependency injection.

That's a spot-on point. The guide recommends these practices, but implementing them demands self-control. When designing components, discipline in our architecture is non-negotiable.

Container Components Aren't the Whole Answer

Even if you split your components into container and presentational categories, you still might be missing a safeguard against excessive responsibility. Business logic belongs in component-level services—be they facades, or more specific services like presenters.

Worth It for Enduring Projects

Is this over-engineering? For a small or basic app, perhaps. But for projects and products built to last, pondering your component architecture early on is a wise move.

By isolating responsibilities into well-defined software artifacts, each piece becomes easier to understand and test. When new requirements inevitably appear, you can extend the specific artifact that owns that concern, at the right level of abstraction.

Case study: Trimming the cart component

Let’s revisit the cart component from the Getting Started guide and see what became of it.

<!-- cart.component.html -->
<h3>Cart</h3>

<p>
  <a routerLink="/shipping">Shipping Prices</a>
</p>

<div class="cart-item" *ngFor="let item of items">
  <span>{{ item.name }} </span>
  <span>{{ item.price | currency }}</span>
</div>

<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit(checkoutForm.value)">
  <div>
    <label for="name">
      Name
    </label>
    <input id="name" type="text" formControlName="name">
  </div>

  <div>
    <label for="address">
      Address
    </label>
    <input id="address" type="text" formControlName="address">
  </div>

  <button class="button" type="submit">Purchase</button>
</form>
Enter fullscreen mode Exit fullscreen mode
Listing 2.1. Cart: Initial mixed component template.
// cart.component.ts
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';

import { CartService } from '../cart.service';

@Component({
  selector: 'app-cart',
  styleUrls: ['./cart.component.css'],
  templateUrl: './cart.component.html',
})
export class CartComponent {
  items;
  checkoutForm;

  constructor(
    private cartService: CartService,
    private formBuilder: FormBuilder,
  ) {
    this.items = this.cartService.getItems();

    this.checkoutForm = this.formBuilder.group({
      name: '',
      address: '',
    });
  }

  onSubmit(customerData) {
    // Process checkout data here
    console.warn('Your order has been submitted', customerData);

    this.items = this.cartService.clearCart();
    this.checkoutForm.reset();
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 2.2. Cart: Initial mixed component model.

Listings 2.1 and 2.2 give us the original picture—a single component juggling duties across multiple horizontal layers. It also mixes logic from different levels of abstraction.

The original cart component bundles together presentation concerns, presentation implementation details, high-level and low-level presentation logic, and low-level user interaction logic. Some of this might be acceptable in a presentational component, but we have clearly reached the point where a refactor is warranted.

It also carries non-presentational logic: state management implementation details and low-level business logic. State management is the first thing we should pull out. Local UI state is the exception—it falls under user interaction, which is part of UI behaviour.

<!-- cart.container.html -->
<app-cart-ui
  [items]="items"
  [shippingPath]="shippingPath"
  (checkout)="onCheckout($event)"></app-cart-ui>
Enter fullscreen mode Exit fullscreen mode
Listing 3.1. Cart: Container component template.
// cart.container.ts
import { Component } from '@angular/core';

import { Customer } from '../customer';
import { pathPrefix, routes } from '../root-routes';
import { CartService } from './cart.service';

@Component({
  selector: 'app-cart',
  templateUrl: './cart.container.html',
})
export class CartContainerComponent {
  items = this.cartService.getItems();
  shippingPath = pathPrefix + routes.shipping.path;

  constructor(
    private cartService: CartService,
  ) {}

  onCheckout(customerData: Customer) {
    // Process checkout data here
    console.warn('Your order has been submitted', customerData);

    this.items = this.cartService.clearCart();
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 3.2. Cart: Container component model.

In Listings 3.1 and 3.2, we have lifted a container component out of the mixed cart component. All state management wiring now lives there.

// root-routes.ts
export const pathPrefix = '/';

export const routes = {
  shipping: {
    path: 'shipping',
  },
};
Enter fullscreen mode Exit fullscreen mode
Listing 3.3 Root routes after extracting from cart component.

In the original cart component, the shipping route was hard-coded in the template. We have since moved the route path into its own module, as shown in Listing 3.3, which makes it reusable and easy to modify.


Hard-coded route URLs and paths in templates and component models is a poor practice.

The better approach is to keep route paths and URLs in a module of their own so components, directives, and services can reference them cleanly.

Routeshub, created by Max Tarsis, is a route management library that plugs into Angular’s router with little friction.


The container component assembles the full route URL and hands it to the presentational cart component, which we will look at next.

<!-- cart.component.html -->
<h3>Cart</h3>

<p>
  <a [routerLink]="shippingPath">Shipping Prices</a>
</p>

<app-cart-item *ngFor="let item of items"
  [item]="item"></app-cart-item>

<app-checkout (checkout)="checkout.emit($event)"></app-checkout>
Enter fullscreen mode Exit fullscreen mode
Listing 4.1. Cart: Presentational component template.
// cart.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';

import { Customer } from '../customer';
import { Products } from '../product';

@Component({
  selector: 'app-cart-ui',
  styleUrls: ['./cart.component.css'],
  templateUrl: './cart.component.html',
})
export class CartComponent {
  @Input()
  items: Products;
  @Input()
  shippingPath: string;
  @Output()
  checkout = new EventEmitter<Customer>();
}
Enter fullscreen mode Exit fullscreen mode
Listing 4.2. Cart: Presentational component model.

Listings 4.1 and 4.2 show the presentational cart component now holding very little logic. The shipping route URL arrives through an input property. This component does not care what the full URL is or how to fetch it.

Along the same lines, it simply iterates over products and passes each one to another presentational component we extracted—the cart item component.

I will skip the details of the even simpler cart item component, but the complete solution lives in a StackBlitz workspace.

We also pulled out yet another presentational component: the checkout component.

<!-- checkout.component.html -->
<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="name">
      Name
    </label>
    <input id="name" type="text" formControlName="name">
  </div>

  <div>
    <label for="address">
      Address
    </label>
    <input id="address" type="text" formControlName="address">
  </div>

  <button class="button" type="submit">Purchase</button>
</form>
Enter fullscreen mode Exit fullscreen mode
Listing 5.1. Cart: Checkout component template.
// checkout.component.ts
import { Component, EventEmitter, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';

import { Customer } from '../customer';
import { CheckoutPresenter } from './checkout.presenter';

@Component({
  selector: 'app-checkout',
  templateUrl: './checkout.component.html',
  viewProviders: [CheckoutPresenter],
})
export class CheckoutComponent {
  @Output()
  checkout = new EventEmitter<Customer>();

  get checkoutForm(): FormGroup {
    return this.presenter.form;
  }

  constructor(
    private presenter: CheckoutPresenter,
  ) {}

  onSubmit() {
    const customerData = this.presenter.checkout();
    this.checkout.emit(customerData);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 5.2. Cart: Checkout component model.

The checkout template in Listing 5.1 wires native form controls to reactive Angular form groups and controls.

The component model in Listing 5.2 exposes the form group from the checkout presenter—a component-level service that wraps user interaction logic.

This presentational component turns a form submission into an output property event by delegating the work to the checkout presenter.

// checkout.presenter.ts
import { Injectable } from '@angular/core';
import { FormBuilder } from '@angular/forms';

import { Customer } from '../../customer';

@Injectable()
export class CheckoutPresenter {
  form = this.formBuilder.group({
    name: '',
    address: '',
  });

  constructor(
    private formBuilder: FormBuilder,
  ) {}

  checkout(): Customer {
    const customerData: Customer = this.form.value;
    this.form.reset();

    return customerData;
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 6. Cart: Checkout presenter.

The low-level task of constructing the reactive checkout form group is tucked inside the checkout presenter in Listing 6. The form is surfaced through a public property.

Checking out is a matter of gathering values from the reactive form controls, resetting the form group, and returning the collected form entries from the checkout method.

I typically expose observables that emit when an action like checkout takes place. That way, the presentational component can bind a button directly to the presenter’s method without worrying about a return value. An output property is then hooked up to the presenter’s observable property instead.

For this case study, I deliberately kept the implementation close to the original solution, so some imperative control flow remains in the checkout component’s form submission handler.

File and directory layout

After separating concerns and extracting interfaces, the initial file set from Figure 2

└── cart
   ├── cart.component.css
   ├── cart.component.html
   ├── cart.component.ts
   └── cart.service.ts

Figure 2. Cart component: Initial file tree.

turned into the files and folders shown in Figure 3.

├── cart
│  ├── cart-item
│  │  ├── cart-item.component.html
│  │  └── cart-item.component.ts
│  ├── checkout
│  │  ├── checkout.component.html
│  │  ├── checkout.component.ts
│  │  └── checkout.presenter.ts
│  ├── cart.component.css
│  ├── cart.component.html
│  ├── cart.component.ts
│  ├── cart.container.html
│  ├── cart.container.ts
│  └── cart.service.ts
├── customer.ts
├── product.ts
└── root-routes.ts

Figure 3. Cart: Final file tree.

The refactored solution is available as a StackBlitz workspace.

Concerns after separation

Figure 4. Cart: Initial concerns.

Figure 4. Cart: Initial concerns.

At the start, a single mixed component held far too many responsibilities, as illustrated in Figure 4. The concerns in the lower portion have no place in a presentational component: state management implementation details, high-level business logic, and low-level business logic.

Figure 5. Cart: Refactored concerns.

Figure 5. Cart: Refactored concerns.

After splitting the cart feature into several components plus a presenter, the concerns are now reasonably separated, as Figure 5 shows.

Each artifact now stays within one horizontal layer, or at most two. They also keep to a single abstraction level in most cases.

There is always room for refinement, but this is a sound component design for this particular application feature.

Style guide recommendations worth revisiting

The official Angular Style Guide advocates for this same approach to component design. Let's take another look at some of its recommendations.

Move non-presentational logic into services

Style 05–15: Delegate complex component logic to services

Within the scope of lean Angular components, few guidelines carry more weight than this one.

The recommendation has two parts. First, it asks us to move logic that isn't about presentation into services. Second, it asks us to keep components simple and dedicated to their intended role. Concretely, that means minimizing logic that lives in templates, moving logic out of component models, and keeping components compact — a thousand-line component is a clear warning sign.

Even a component with only a hundred lines should trigger some caution, or at least a moment of reflection on whether splitting it into several components or delegating parts to services might be the better path.

Reuse is a major motivation behind moving logic into services. Data services and services that manage application state are strong candidates for reuse — possibly across different parts of an app, or even across multiple projects.

Similarly, once non-presentational logic has been pulled out of a component, what remains is a presentational component that can be paired with state coming from various parts of an application — or from an entirely different project.

Testing is another reason to make this move. Services that stand on their own are faster and more straightforward to test in isolation. A component that only handles presentation concerns is likewise easier to test, because dependencies have been relocated and implementation details sit outside the component.

The final point this guideline leaves us with: extracting logic from components keeps them slim, sharp, and well-targeted.

The goal is components that are lean, mean Angular-powered machines.

Keep presentation logic out of the template

Style 05–17: Put presentation logic in the component class

Logic that addresses the same concern at the same abstraction level belongs close together. Component templates, styles, and models are closely tied to each other, but each has its own responsibility.

A template should concern itself with declarative DOM manipulation and event binding, not with the inner workings of the component. The component model, for its part, should expose properties that the template can bind to, keeping implementation details hidden from the template side.

The style guide also points out that when presentation logic sits in the component model instead of the template, we get better reusability, maintainability, and testability — qualities that are always worth pursuing.

Choose a directive over a component when it suffices

Style 06–01: Use directives to enhance an element

This recommendation is a reminder that components aren't always the right first instinct. When there's no template to speak of, or the DOM modifications can be handled directly on the host element, an attribute directive can serve us well.

Replacing a component with an attribute directive removes one DOM element per directive instance. In complex applications, or when a particular DOM structure is required, that saving can matter a great deal.

When content should appear based on a certain state or set of conditions, structural directives are a natural fit.

Directives do have a limitation compared to components: they can't be applied dynamically. Components, on the other hand, can be rendered dynamically without issue.

On the upside, as the guideline notes, a template element can carry multiple directives at once. A component, by contrast, can be applied to a template element only one at a time.

One task, done properly

Style 07–02: Single responsibility

This recommendation points us toward the Single Responsibility Principle. Whether the two align depends on how we interpret responsibility — a discussion I'll leave aside for now, even though it's an important one.

My reading of this style recommendation is that services should encapsulate logic from a single horizontal layer at a single level of abstraction.

The Angular Style Guide itself frames the idea in its introduction:

Apply the Single Responsibility Principle to all components, services, and other symbols. This helps make the app cleaner, easier to read and maintain, and more testable.

What the guide doesn't offer is a method for doing so — that's a subject for another piece.

Services at the component level

Style 07–03: Providing a service

The style guide encourages the use of a root-provided Injectable decorator for services, which makes them singletons capable of sharing business logic and state across various parts of an application.

That said, this setup doesn't work well when two different components need separate instances of a service. In such cases, the better approach is to provide the service at the component level that requires its own distinct instance.

What's more interesting is the guide's advice: when separate service instances are needed in different components, the service should be provided at the component level. Whether those components use the same component model or different ones, the way to do this is through the providers or viewProviders option on the Component decorator factory.


For a thorough explanation of Angular providers, read “Tree-shakable dependencies in Angular projects.”


Delegate non-presentational concerns to services

Style 08–01: Talk to the server through a service

This guideline calls for moving data operations and interaction logic into a service. That includes HTTP communication, reads and writes to web storage, and in-memory data stores like those used in Flux-style architectures.

The component's role is to present information and gather what's needed for the view. It shouldn't concern itself with where the data comes from, only with which service to ask. Moving data logic into a data service simplifies the component and keeps it focused on the view.

This is fundamentally about separation of concerns. The idea is not to handle different horizontal layers of an application in one place. Components should be built for presentation only. I go a step further and extract presentation and user interaction as well, letting component-level services like presenters take on those duties.

The style guide also reminds us that pulling logic out of a component into an abstract interface makes the component easier to test.

By letting a component hand off data retrieval and storage to a service, the component avoids having to know the details. That separation of concerns also makes it much easier to change how data is handled without breaking the component itself.

Key takeaways for lean Angular components

For smaller projects or codebases with a limited lifecycle, mixed components can be perfectly fine. In such contexts, maintainability, scalability and testability are not as critical, so a more blended approach does not create meaningful problems.

When working on larger, more intricate applications, however, those qualities become essential. To maximise them, we should turn to presentational components that restrict themselves to logic from the presentational, horizontal layers of the application. Each of these components should operate at a single level of abstraction and focus on one concern only.

Lean presentational components exist solely to display information to users and to let them interact with the application. The details of how that happens are handled elsewhere, in different horizontal layers. When presentation or interaction requires complex logic, that work is moved to component-scoped services like presenters.

Lean container components can follow the same principle. They expose application state to presentational components and translate application-specific events into commands that mutate that state. Any intricate logic here is pushed to an application-level service such as a facade, or possibly to a component-level dependency like a data mapper.


There is another way to keep non-presentational logic out of components: the BLoC (Business Logic Component) pattern.

For a thorough explanation of this approach, see Suguru Inatomi’s write-up titled “BLoC design pattern with Angular”.

I also shared my own thoughts in this follow-up thread. There I compare BLoCs with container components, presentational components and presenters, and I propose a few refinements to Suguru’s original pattern.


Adopting this style means our codebase will contain more classes in total. But each one has a narrow, well-defined role in the overall application flow. Individual components become easier to understand, and every dependency is straightforward to stub or mock during testing.

We took these principles and applied them to the cart component from the "Forms" section of the official Getting Started guide. The result was a sturdier component architecture, one that aligns with recognised best practices and improves maintainability, testability and scalability.

In the end, we arrived at lean Angular components where responsibilities are distributed across many classes, each of which stays simple and focused. The balance of concerns is far more sensible than what we started with.

To close things out, we looked at selected recommendations from the style guide. If you only read one guideline, make it Style 05–15: Delegate complex component logic to services.

We also saw that lean Angular components are supported by the architecture guidance for Angular services and dependency injection.

This is not just my opinion. Ward Bell is the original author of the Angular architectural guide, and he weighs in on the matter in this Twitter thread.

The goal, then, is to strip logic out of our components until there is barely anything left to test. We want to build a lean, efficient Angular application. We want components that are deliberately designed to stay simple.

Further reading

One practical way to achieve lean Angular components is through the combination of container components, presentational components and presenters. I walk through this setup in the opening piece, “Model-View-Presenter with Angular”.

Credits and thanks

A big thank you to Ward Bell for generously sharing his experience with the community and for taking the time to answer my questions about his perspective on this subject.

Reviewers

Many thanks to my fellow Angular experts who helped sharpen this article: