Back to Blog
design-patternscsharptypescriptstructural-patterns

Decorator Pattern — FixIt Pro Series #09

Adding urgency, insurance, and warranty to a job card at runtime, without modifying the original class or creating a subclass for every combination. Learn the Decorator pattern in C# and TypeScript.


Series: Design Patterns with FixIt Pro  ·  Episode 09 / 22  ·  Structural Pattern
Previous: #08 — Composite Pattern


The Scenario

FixIt Pro job cards can have optional extras applied to them:

  • Urgent — surcharge applied, handyman notified immediately
  • Insured — liability coverage added, paperwork generated
  • Warrantied — 90-day callback guarantee added to the job

These extras can be combined. A job can be urgent and insured. Or warrantied and urgent. Or all three at once.

The naive approach is subclassing:

UrgentJobCard
InsuredJobCard
WarrantiedJobCard
UrgentInsuredJobCard
UrgentWarrantiedJobCard
InsuredWarrantiedJobCard
UrgentInsuredWarrantiedJobCard

Seven classes for three extras. Add a fourth — say, Priority Scheduling — and you need 15. The explosion is exponential.

Sound familiar? It's the same problem the Bridge pattern solved in Episode 7 — but this time the dimensions aren't two independent hierarchies. They're optional layers wrapped around a single object. That's the Decorator.


What Is the Decorator Pattern?

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

A Decorator wraps an object, implements the same interface, and adds behaviour before or after delegating to the wrapped object. You can stack decorators — each one wraps the previous — building up behaviour layer by layer.

The four participants

Role FixIt Pro equivalent
Component interface IJobCard — the interface all job cards implement
Concrete Component JobCard — the base job card being decorated
Base Decorator JobCardDecorator — wraps an IJobCard, delegates by default
Concrete Decorators UrgentDecorator, InsuredDecorator, WarrantiedDecorator

C# Implementation

// ── Component Interface ────────────────────────────────────

public interface IJobCard
{
    string  JobId       { get; }
    string  Title       { get; }
    string  Category    { get; }
    decimal Cost        { get; }
    string  Description { get; }
    void    Execute();
}

// ── Concrete Component ─────────────────────────────────────

public class JobCard : IJobCard
{
    public string  JobId       { get; } = Guid.NewGuid().ToString()[..8];
    public string  Title       { get; }
    public string  Category    { get; }
    public decimal Cost        { get; }
    public string  Description => $"[{Category}] {Title}";

    public JobCard(string title, string category, decimal cost)
    {
        Title    = title;
        Category = category;
        Cost     = cost;
    }

    public void Execute() =>
        Console.WriteLine($"⚙️  Executing: {Description} — N${Cost:N2}");
}

// ── Base Decorator ─────────────────────────────────────────
// Wraps an IJobCard and delegates all calls to it by default.
// Concrete decorators only override what they need to change.

public abstract class JobCardDecorator : IJobCard
{
    protected readonly IJobCard _wrapped;

    protected JobCardDecorator(IJobCard jobCard)
    {
        _wrapped = jobCard;
    }

    // Delegate everything to the wrapped component by default
    public virtual string  JobId       => _wrapped.JobId;
    public virtual string  Title       => _wrapped.Title;
    public virtual string  Category    => _wrapped.Category;
    public virtual decimal Cost        => _wrapped.Cost;
    public virtual string  Description => _wrapped.Description;
    public virtual void    Execute()   => _wrapped.Execute();
}

// ── Concrete Decorators ────────────────────────────────────

public class UrgentDecorator : JobCardDecorator
{
    private const decimal SurchargeRate = 0.25m; // 25% surcharge

    public UrgentDecorator(IJobCard jobCard) : base(jobCard) { }

    public override decimal Cost        => _wrapped.Cost * (1 + SurchargeRate);
    public override string  Description => $"🚨 URGENT | {_wrapped.Description}";

    public override void Execute()
    {
        Console.WriteLine("  📟 Paging on-call handyman immediately...");
        base.Execute();
        Console.WriteLine("  ⚡ Urgent flag set — supervisor notified.");
    }
}

public class InsuredDecorator : JobCardDecorator
{
    private const decimal InsurancePremium = 350m;

    public InsuredDecorator(IJobCard jobCard) : base(jobCard) { }

    public override decimal Cost        => _wrapped.Cost + InsurancePremium;
    public override string  Description => $"🛡️  INSURED | {_wrapped.Description}";

    public override void Execute()
    {
        Console.WriteLine("  📄 Generating liability waiver...");
        base.Execute();
        Console.WriteLine("  ✅ Insurance certificate issued.");
    }
}

public class WarrantiedDecorator : JobCardDecorator
{
    private const int WarrantyDays = 90;

    public WarrantiedDecorator(IJobCard jobCard) : base(jobCard) { }

    public override string Description =>
        $"🔒 WARRANTIED ({WarrantyDays}d) | {_wrapped.Description}";

    public override void Execute()
    {
        base.Execute();
        Console.WriteLine($"  📆 90-day callback warranty registered for Job #{JobId}.");
    }
}

// ── Client Code ────────────────────────────────────────────

class Program
{
    static void Main()
    {
        // Base job card
        IJobCard job = new JobCard("Replace burst pipe under sink", "Plumbing", 850m);

        Console.WriteLine("=== Base Job ===");
        Console.WriteLine($"  {job.Description}");
        Console.WriteLine($"  Cost: N${job.Cost:N2}");
        job.Execute();

        // Wrap with Urgent
        IJobCard urgentJob = new UrgentDecorator(job);
        Console.WriteLine("\n=== + Urgent ===");
        Console.WriteLine($"  {urgentJob.Description}");
        Console.WriteLine($"  Cost: N${urgentJob.Cost:N2}");
        urgentJob.Execute();

        // Wrap with Urgent + Insured
        IJobCard urgentInsuredJob = new InsuredDecorator(new UrgentDecorator(job));
        Console.WriteLine("\n=== + Urgent + Insured ===");
        Console.WriteLine($"  {urgentInsuredJob.Description}");
        Console.WriteLine($"  Cost: N${urgentInsuredJob.Cost:N2}");
        urgentInsuredJob.Execute();

        // Wrap with all three
        IJobCard fullyDecoratedJob =
            new WarrantiedDecorator(
                new InsuredDecorator(
                    new UrgentDecorator(job)));

        Console.WriteLine("\n=== + Urgent + Insured + Warrantied ===");
        Console.WriteLine($"  {fullyDecoratedJob.Description}");
        Console.WriteLine($"  Cost: N${fullyDecoratedJob.Cost:N2}");
        fullyDecoratedJob.Execute();
    }
}

Output:

=== Base Job ===
  [Plumbing] Replace burst pipe under sink
  Cost: N$850.00
⚙️  Executing: [Plumbing] Replace burst pipe under sink — N$850.00

=== + Urgent ===
  🚨 URGENT | [Plumbing] Replace burst pipe under sink
  Cost: N$1,062.50
  📟 Paging on-call handyman immediately...
⚙️  Executing: [Plumbing] Replace burst pipe under sink — N$850.00
  ⚡ Urgent flag set — supervisor notified.

=== + Urgent + Insured ===
  🛡️  INSURED | 🚨 URGENT | [Plumbing] Replace burst pipe under sink
  Cost: N$1,412.50
  📄 Generating liability waiver...
  📟 Paging on-call handyman immediately...
⚙️  Executing: [Plumbing] Replace burst pipe under sink — N$850.00
  ⚡ Urgent flag set — supervisor notified.
  ✅ Insurance certificate issued.

=== + Urgent + Insured + Warrantied ===
  🔒 WARRANTIED (90d) | 🛡️  INSURED | 🚨 URGENT | [Plumbing] Replace burst pipe under sink
  Cost: N$1,412.50
  📄 Generating liability waiver...
  📟 Paging on-call handyman immediately...
⚙️  Executing: [Plumbing] Replace burst pipe under sink — N$850.00
  ⚡ Urgent flag set — supervisor notified.
  ✅ Insurance certificate issued.
  📆 90-day callback warranty registered for Job #f3a12b9c.

TypeScript Implementation

// ── Component/IJobCard.ts ──────────────────────────────────

export interface IJobCard {
  readonly jobId:       string;
  readonly title:       string;
  readonly category:    string;
  readonly cost:        number;
  readonly description: string;
  execute(): void;
}

// ── Leaf/JobCard.ts ────────────────────────────────────────

import { IJobCard } from "../Component/IJobCard";

const shortId = () => Math.random().toString(36).slice(2, 10);

export class JobCard implements IJobCard {
  readonly jobId = shortId();

  constructor(
    readonly title:    string,
    readonly category: string,
    readonly cost:     number
  ) {}

  get description(): string {
    return `[${this.category}] ${this.title}`;
  }

  execute(): void {
    console.log(`⚙️  Executing: ${this.description} — N$${this.cost.toFixed(2)}`);
  }
}

// ── Decorator/JobCardDecorator.ts ──────────────────────────

import { IJobCard } from "../Component/IJobCard";

export abstract class JobCardDecorator implements IJobCard {
  constructor(protected readonly wrapped: IJobCard) {}

  get jobId():       string { return this.wrapped.jobId; }
  get title():       string { return this.wrapped.title; }
  get category():    string { return this.wrapped.category; }
  get cost():        number { return this.wrapped.cost; }
  get description(): string { return this.wrapped.description; }

  execute(): void { this.wrapped.execute(); }
}

// ── Decorator/UrgentDecorator.ts ───────────────────────────

import { JobCardDecorator } from "./JobCardDecorator";
import { IJobCard          } from "../Component/IJobCard";

export class UrgentDecorator extends JobCardDecorator {
  private static readonly SURCHARGE_RATE = 0.25;

  constructor(jobCard: IJobCard) { super(jobCard); }

  get cost():        number { return this.wrapped.cost * (1 + UrgentDecorator.SURCHARGE_RATE); }
  get description(): string { return `🚨 URGENT | ${this.wrapped.description}`; }

  execute(): void {
    console.log("  📟 Paging on-call handyman immediately...");
    super.execute();
    console.log("  ⚡ Urgent flag set — supervisor notified.");
  }
}

// ── Decorator/InsuredDecorator.ts ──────────────────────────

import { JobCardDecorator } from "./JobCardDecorator";
import { IJobCard          } from "../Component/IJobCard";

export class InsuredDecorator extends JobCardDecorator {
  private static readonly INSURANCE_PREMIUM = 350;

  constructor(jobCard: IJobCard) { super(jobCard); }

  get cost():        number { return this.wrapped.cost + InsuredDecorator.INSURANCE_PREMIUM; }
  get description(): string { return `🛡️  INSURED | ${this.wrapped.description}`; }

  execute(): void {
    console.log("  📄 Generating liability waiver...");
    super.execute();
    console.log("  ✅ Insurance certificate issued.");
  }
}

// ── Decorator/WarrantiedDecorator.ts ───────────────────────

import { JobCardDecorator } from "./JobCardDecorator";
import { IJobCard          } from "../Component/IJobCard";

export class WarrantiedDecorator extends JobCardDecorator {
  private static readonly WARRANTY_DAYS = 90;

  constructor(jobCard: IJobCard) { super(jobCard); }

  get description(): string {
    return `🔒 WARRANTIED (${WarrantiedDecorator.WARRANTY_DAYS}d) | ${this.wrapped.description}`;
  }

  execute(): void {
    super.execute();
    console.log(`  📆 90-day callback warranty registered for Job #${this.jobId}.`);
  }
}

// ── App.ts ─────────────────────────────────────────────────

import { JobCard             } from "./Leaf/JobCard";
import { UrgentDecorator     } from "./Decorator/UrgentDecorator";
import { InsuredDecorator    } from "./Decorator/InsuredDecorator";
import { WarrantiedDecorator } from "./Decorator/WarrantiedDecorator";
import { IJobCard            } from "./Component/IJobCard";

const job: IJobCard = new JobCard("Replace burst pipe under sink", "Plumbing", 850);

console.log("=== Base Job ===");
console.log(`  ${job.description}`);
console.log(`  Cost: N$${job.cost.toFixed(2)}`);
job.execute();

const urgentJob: IJobCard = new UrgentDecorator(job);
console.log("\n=== + Urgent ===");
console.log(`  ${urgentJob.description}`);
console.log(`  Cost: N$${urgentJob.cost.toFixed(2)}`);
urgentJob.execute();

const urgentInsuredJob: IJobCard = new InsuredDecorator(new UrgentDecorator(job));
console.log("\n=== + Urgent + Insured ===");
console.log(`  ${urgentInsuredJob.description}`);
console.log(`  Cost: N$${urgentInsuredJob.cost.toFixed(2)}`);
urgentInsuredJob.execute();

const fullyDecoratedJob: IJobCard =
  new WarrantiedDecorator(
    new InsuredDecorator(
      new UrgentDecorator(job)));

console.log("\n=== + Urgent + Insured + Warrantied ===");
console.log(`  ${fullyDecoratedJob.description}`);
console.log(`  Cost: N$${fullyDecoratedJob.cost.toFixed(2)}`);
fullyDecoratedJob.execute();

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Base decorator abstract class with virtual properties abstract class with get accessors
Delegation virtual keyword allows overriding Getter override via get propertyName()
Calling wrapped base.Execute() super.execute()
Static constant private const decimal SurchargeRate = 0.25m private static readonly SURCHARGE_RATE = 0.25
Override keyword Required — override Not required — just redefine the getter

One subtlety: in TypeScript, getters in subclasses automatically shadow parent getters — no explicit override keyword needed. In C#, the virtual + override contract is strictly enforced at compile time.


Decorator vs Inheritance vs Bridge

Inheritance Bridge Decorator
Mechanism Subclass per variant Two independent hierarchies Wrapping layers
When decided Compile time Design time Runtime
Combinations Exponential subclasses Additive (M + N) Additive (stackable)
FixIt Pro 7 subclasses for 3 extras Urgency × Channel Wrap base card with extras
Best for Fixed, known variants Two varying dimensions Optional, stackable behaviour

When to Use the Decorator

Use it when:

  • You want to add behaviour to individual objects without affecting others of the same class
  • Extension by subclassing would lead to a combinatorial explosion of classes
  • You need to add and remove responsibilities at runtime

Avoid it when:

  • The order of wrapping matters in ways that are hard to control — deeply nested decorators can become hard to debug
  • You need to inspect or unwrap the decorator chain — the interface hides what's underneath
  • A simple flag or configuration object on the base class would do the job just as cleanly

Real-World Takeaway

The Decorator is one of the most widely used patterns in the wild. In .NET, Stream decorators — BufferedStream, GZipStream, CryptoStream — all wrap a Stream and add behaviour. ASP.NET Core middleware is a decorator chain — each middleware wraps the next. In JavaScript, Express.js middleware works the same way. console.log wrappers, logging decorators in NestJS (@Injectable with interceptors), and Python's @decorator syntax are all variations of this pattern.

In FixIt Pro, a job card can go from a plain JobCard to an urgent, insured, warrantied job by wrapping it three times. The base object never changes. New extras are new classes. The dispatcher doesn't need to know what's wrapped — it just calls Execute().


Repo Structure for This Episode

github.com/antonlungameni/fixit-pro-design-patterns

fixit-pro-design-patterns/
├── csharp/Structural/09-Decorator/
│   ├── Component/IJobCard.cs
│   ├── Leaf/JobCard.cs
│   ├── Decorator/JobCardDecorator.cs
│   ├── Decorator/UrgentDecorator.cs
│   ├── Decorator/InsuredDecorator.cs
│   ├── Decorator/WarrantiedDecorator.cs
│   └── Program.cs
└── typescript/Structural/09-Decorator/
    ├── Component/IJobCard.ts
    ├── Leaf/JobCard.ts
    ├── Decorator/JobCardDecorator.ts
    ├── Decorator/UrgentDecorator.ts
    ├── Decorator/InsuredDecorator.ts
    ├── Decorator/WarrantiedDecorator.ts
    └── App.ts

Previous: #08 — Composite Pattern
Next up: #10 — Facade Pattern
A JobDispatchFacade that hides the complexity of scheduling, notifying, and assigning handymen behind a single, clean interface.