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.