Blog

Composition over Inheritance

Angular
Composition over Inheritance

You've got to roll with it

When I started learing to code, I was taught about inheritance and how it can be used to share code between classes. I tought it was a great way to structure and reuse functionanlity. But as I gained more experience and started working professionally, I began to see the downsides of inheritance—how it can lead to overly complex, tightly coupled, and difficult-to-maintain code. This is especially true in frontend development, where requirements change rapidly, and the codebase needs to adapt without breaking existing functionality or turning into an unmanageable mess.

A very powerful concept that can help us avoid the pitfalls of inheritance is composition. Both inheritance and composition aim to achieve code reuse, but they fundamentally differ in how they accomplish it:

  • Inheritance creates a hierarchy where a child class derives from a parent class, inheriting all its properties and methods.
  • Composition assembles smaller, reusable pieces of code to build features flexibly.

Inheritance defines an "is-a" relationship, while composition defines a "has-a" relationship. With inheritance we create a tight coupling between classes.

At first glance, inheritance seems like it has its place. And it does. But in my experience, almost every problem that can be solved with inheritance can be solved better with composition.

Why? Because, inheritance has a big enemy, and that is CHANGE.

If there’s one thing I’ve learned about software development, it’s that the only true constant is CHANGE. Businesses grow, markets shift, user needs evolve, and new technologies emerge. All of these factors drive the need for software to adapt.

So your code should be able to "roll with it".

A little story

Imagine we have an application that displays user data. We currently have 2 types of users: "Moderators" and an "Admins". We decide to use inheritance to share logic between them

// BaseUserComponent (shared logic for all users)
export abstract class BaseUserComponent {
  userName = '';
  userPermissions: string[] = [];

  loadUserDetails(): void {
    // Common logic for loading user details
  }

  setUserPermissions(): void {
    // Common logic for setting user permissions
  }

  abstract getDashboardMessage(): string; // Forces child classes to implement
}
// AdminComponent (inherits from BaseUserComponent)
@Component({
  selector: 'app-admin',
  template: `<h1>Welcome, Admin {{ userName }}</h1>
    <p>{{ getDashboardMessage() }}</p>`
})
export class AdminComponent extends BaseUserComponent {
  getDashboardMessage(): string {
    return 'You have full access to the system.';
  }
}

// ModeratorComponent (inherits from BaseUserComponent)
@Component({
  selector: 'app-moderator',
  template: `<h1>Welcome, Moderator {{ userName }}</h1>
    <p>{{ getDashboardMessage() }}</p>`
})
export class ModeratorComponent extends BaseUserComponent {
  getDashboardMessage(): string {
    return 'You can manage user content.';
  }
}

This solution works fine, but now the product owner comes to us and says that we need to add a new user type: "guests". Guests can only view content, don’t have specific user details, and don’t even have access to the dashboard. Now we have a problem:

  • If we create a GuestComponent that extends BaseUserComponent, it will be forced to implement getDashboardMessage(), even though it doesn’t need it.
  • The more user types we add, the more this inheritance structure will break down, leading to complex hierarchies or bloated base classes filled with unnecessary conditionals (if-else everywhere).

This is a very simplistic example, but in real world applications, things like this happen all the time. Managing these changes becomes really difficult, mainly because of the tight coupling that inheritance gives us.

Instead of inheritance, let's use composition, a much more flexible approach. Angular provides us with powerful tools to utilize composition.

use the power of Angular to compose

Dependency Injection

A great way to share logic in Angular is Dependency Injection (DI). Let’s refactor our example using a UserService instead of inheritance:

// UserService (handles shared logic)
@Injectable()
export class UserService {
  loadUserDetails(): void {
    console.log('Loading details for');
  }
}

// AdminComponent
@Component({
  selector: 'app-admin',
  template: `<h1>Welcome, Admin {{ userName }}</h1>
    <p>{{ getDashboardMessage() }}</p>`
})
export class AdminComponent {
  readonly #userService = inject(UserService);

  loadUserDetails(): void {
    this.#userService.loadUserDetails();
  }

  getDashboardMessage(): string {
    return 'You have full access to the system.';
  }
}

// ModeratorComponent
@Component({
  selector: 'app-moderator',
  template: `<h1>Welcome, Moderator {{ userName }}</h1>
    <p>{{ getDashboardMessage() }}</p>`
})
export class ModeratorComponent {
  readonly #userService = inject(UserService);

  loadUserDetails(): void {
    this.#userService.loadUserDetails();
  }

  getDashboardMessage(): string {
    return 'You can manage user content.';
  }
}

As you can see, we removed the base class and instead we use a service to handle the shared logic:

  • Removed tight coupling between components.
  • Can easily add a GuestComponent without unnecessary inherited methods.
  • Reuse logic via a service, making it more maintainable.

Directives

Because we are frontend developers, we also have to present the data to the user.

Angular directives are a great way to share presentation logic across multiple components. Directives are typically used to add behavior or modify the appearance of an existing elements. Example: A simple CharacterCounterDirective that adds a character counter to an input field.

@Directive({ selector: '[characterCounter]' })
export class CharacterCounterDirective {
  value = input.required<string>({ alias: 'characterCounter' });

  characterCount = computed(() => this.value.length);

  // logic to add characterCount to input field
  // ...
}
@Component({
  selector: 'app-form-input',
  imports: [CharacterCounterDirective],
  template: `<input type="text" [characterCounter]="value" />`
})
export class FormInputComponent {}

This example is far from complete, but it shows how you can use directives to add functionality to elements. Other great use cases for directives are for example: form validatores, dynamic styling, animations, ... .

pipes

Pipes are great for transforming data in your templates. We have a lot of built-in pipes that Angular provides, like DatePipe, CurrencyPipe, UpperCasePipe, ... . But we can also create our own custom pipes. For example a PhonePipe that formats a phone number.

@Pipe({ name: 'phone' })
export class PhonePipe implements PipeTransform {
  transform(rawPhoneNumber) {
    // logic to format phone number
    // ...

    return formattedPhoneNumber;
  }
}
Your Phone Number: <input [(ngModel)]="myNumber" />
<p>Formatted Phone Number: <b>{{ myNumber | phone }}</b></p>

Content projection

Another powerful tool to reuse code, is Content Projection. We can project our custom template inside another component.

Here’s a reusable CardComponent:

// card.component.ts
@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="card-header"></ng-content>
      </div>
      <div class="card-content">
        <ng-content></ng-content>
      </div>
    </div>
  `
})
export class CardComponent {}
<!-- Using the card component -->
<app-card>
  <card-header>Title</card-header>
  This is the content of the card
</app-card>

This makes our UI components more reusable and flexible!

Conclusion

You can't predict the future and how requirements will change, so it's important to write code that is easy to change and maintain.

These examples are very simplistic and every topic has a lot more to offer. The point of this post is to show, you will probably never need inheritance in Angular and can use composition instead.

Inheritance is often misused and leads to tightly coupled, rigid code. While it seems like a good idea at first, in practice:

❌ It is the strongest form of coupling. Changing the base class can break inheriting classes.
❌ It is difficult to modify or extend existing functionality.
❌ It complicates testing due to dependencies in the class hierarchy.

That’s why I prefer using composition.

Composition is the principle of building complete features composed of smaller, reusable pieces of code.

✅ It promotes modularity and reusability.
✅ It simplifies testing and debugging.
✅ It enables dynamic and flexible behavior combinations.
✅ It avoids the pitfalls of deep inheritance hierarchies.

🎶 Bonus: did you get the song reference?

The title and header image are inpired by my favorite band, Oasis. The song "Roll with it" tells us we need to adapt to change. just like composition helps us do in software. 🚀

Oasis - Roll with it