Back to Blog
design-patternscsharptypescriptstructural-patterns

Proxy Pattern — FixIt Pro Series #12

A handyman shouldn't access electrical job cards without certification. Learn how the Proxy pattern adds access control, logging, and lazy loading without changing the original class in C# and TypeScript.


Series: Design Patterns with FixIt Pro  ·  Episode 12 / 22  ·  Structural Pattern
Previous: #11 — Flyweight Pattern


The Scenario

FixIt Pro has a certification problem.

Electrical job cards contain sensitive wiring diagrams, circuit specs, and safety procedures. A general handyman without electrical certification shouldn't be able to open them. But the JobCardRegistry doesn't know anything about certifications — it's a data store, not an access control system.

You could add certification checks directly into JobCardRegistry. But now you're mixing concerns — a registry's job is to store and retrieve, not to enforce business rules. And what about logging? Every time a job card is accessed, the audit trail should record who accessed it and when. That's another concern the registry shouldn't own.

You need something that sits in front of the registry, controls access, and adds cross-cutting behaviour — without changing the registry itself.

That's the Proxy.


What Is the Proxy Pattern?

Provide a surrogate or placeholder for another object to control access to it.

The Proxy implements the same interface as the real object. The client talks to the proxy, thinking it's talking to the real thing. The proxy decides what to do — pass through, block, log, cache, or defer — before optionally delegating to the real object.

Three common Proxy types

Type What it does FixIt Pro use
Protection Proxy Controls access based on permissions Certification check before accessing electrical jobs
Logging Proxy Records calls for audit purposes Log every job card access with timestamp and user
Virtual Proxy Defers expensive object creation until needed Lazy-load job card details only when accessed

We'll implement all three in this episode.

The three participants

Role FixIt Pro equivalent
Subject interface IJobCardAccess — the interface both real and proxy implement
Real Subject JobCardRegistry — the actual registry
Proxy JobCardAccessProxy — wraps the registry, adds behaviour

C# Implementation

// ── Subject Interface ──────────────────────────────────────

public interface IJobCardAccess
{
    JobCard? GetJobCard(string jobId);
    void     Register(JobCard card);
    IReadOnlyList<JobCard> GetAll();
}

// ── JobCard (simplified) ───────────────────────────────────

public class JobCard
{
    public string JobId    { get; init; } = Guid.NewGuid().ToString()[..8];
    public string Title    { get; init; } = string.Empty;
    public string Category { get; init; } = string.Empty;
    public string Status   { get; set;  } = "Active";

    public override string ToString() =>
        $"[{Category}] #{JobId} — {Title} ({Status})";
}

// ── Real Subject ───────────────────────────────────────────

public class JobCardRegistry : IJobCardAccess
{
    private readonly Dictionary<string, JobCard> _store = new();

    public void Register(JobCard card)
    {
        _store[card.JobId] = card;
        Console.WriteLine($"  [Registry] Registered: {card}");
    }

    public JobCard? GetJobCard(string jobId)
    {
        _store.TryGetValue(jobId, out var card);
        return card;
    }

    public IReadOnlyList<JobCard> GetAll() =>
        _store.Values.ToList().AsReadOnly();
}

// ── Handyman (used by the proxy for access control) ────────

public class Handyman
{
    public string       Name             { get; }
    public List<string> Certifications   { get; }

    public Handyman(string name, params string[] certifications)
    {
        Name           = name;
        Certifications = certifications.ToList();
    }

    public bool HasCertification(string category) =>
        Certifications.Any(c => c.Equals(category, StringComparison.OrdinalIgnoreCase));
}

// ── Protection + Logging Proxy ─────────────────────────────

public class JobCardAccessProxy : IJobCardAccess
{
    private readonly IJobCardAccess    _registry;
    private readonly Handyman          _handyman;

    // Categories requiring certification
    private static readonly HashSet<string> _restricted =
        new(StringComparer.OrdinalIgnoreCase) { "Electrical", "Plumbing" };

    public JobCardAccessProxy(IJobCardAccess registry, Handyman handyman)
    {
        _registry = registry;
        _handyman = handyman;
    }

    public void Register(JobCard card)
    {
        Log($"REGISTER — {card}");
        _registry.Register(card);
    }

    public JobCard? GetJobCard(string jobId)
    {
        var card = _registry.GetJobCard(jobId);

        if (card is null)
        {
            Log($"GET #{jobId} — not found.");
            return null;
        }

        if (_restricted.Contains(card.Category) &&
            !_handyman.HasCertification(card.Category))
        {
            Log($"ACCESS DENIED — {_handyman.Name} lacks {card.Category} certification for Job #{jobId}.");
            throw new UnauthorizedAccessException(
                $"{_handyman.Name} is not certified for {card.Category} jobs.");
        }

        Log($"ACCESS GRANTED — {_handyman.Name} accessed {card}.");
        return card;
    }

    public IReadOnlyList<JobCard> GetAll()
    {
        Log($"GET ALL — {_handyman.Name} listed all job cards.");
        return _registry.GetAll();
    }

    private void Log(string message) =>
        Console.WriteLine($"  [{DateTime.Now:HH:mm:ss}] [Proxy] {message}");
}

// ── Virtual Proxy (lazy loading) ───────────────────────────
// Defers creating the real registry until the first actual access.

public class LazyJobCardRegistry : IJobCardAccess
{
    private IJobCardAccess? _realRegistry;

    private IJobCardAccess Registry =>
        _realRegistry ??= CreateRegistry();

    private IJobCardAccess CreateRegistry()
    {
        Console.WriteLine("  [LazyProxy] Initialising real registry (first access)...");
        return new JobCardRegistry();
    }

    public void     Register(JobCard card)          => Registry.Register(card);
    public JobCard? GetJobCard(string jobId)         => Registry.GetJobCard(jobId);
    public IReadOnlyList<JobCard> GetAll()           => Registry.GetAll();
}

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

class Program
{
    static void Main()
    {
        // ── Setup ──────────────────────────────────────────
        var realRegistry = new JobCardRegistry();

        var plumbingJob    = new JobCard { Title = "Replace burst pipe",       Category = "Plumbing"   };
        var electricalJob  = new JobCard { Title = "Install circuit breaker",  Category = "Electrical" };
        var carpentryJob   = new JobCard { Title = "Fit new cabinet frames",   Category = "Carpentry"  };

        realRegistry.Register(plumbingJob);
        realRegistry.Register(electricalJob);
        realRegistry.Register(carpentryJob);

        // ── Certified handyman ─────────────────────────────
        var certifiedHandyman = new Handyman("James Nghipandua", "Electrical", "Plumbing");
        var certifiedProxy    = new JobCardAccessProxy(realRegistry, certifiedHandyman);

        Console.WriteLine("\n=== Certified Handyman Access ===");
        certifiedProxy.GetJobCard(electricalJob.JobId);
        certifiedProxy.GetJobCard(plumbingJob.JobId);
        certifiedProxy.GetJobCard(carpentryJob.JobId);

        // ── Uncertified handyman ───────────────────────────
        var uncertifiedHandyman = new Handyman("Tom Kamati", "Carpentry");
        var uncertifiedProxy    = new JobCardAccessProxy(realRegistry, uncertifiedHandyman);

        Console.WriteLine("\n=== Uncertified Handyman Access ===");
        uncertifiedProxy.GetJobCard(carpentryJob.JobId); // allowed

        try
        {
            uncertifiedProxy.GetJobCard(electricalJob.JobId); // blocked
        }
        catch (UnauthorizedAccessException ex)
        {
            Console.WriteLine($"  ❌ Caught: {ex.Message}");
        }

        // ── Virtual proxy ──────────────────────────────────
        Console.WriteLine("\n=== Virtual Proxy (Lazy Loading) ===");
        Console.WriteLine("  LazyRegistry created — no real registry yet.");

        var lazyRegistry = new LazyJobCardRegistry();
        Console.WriteLine("  First access triggers initialisation...");
        lazyRegistry.Register(new JobCard { Title = "Paint exterior walls", Category = "General" });
    }
}

Output:

=== Certified Handyman Access ===
  [09:14:22] [Proxy] ACCESS GRANTED — James Nghipandua accessed [Electrical] #a3f12b — Install circuit breaker (Active).
  [09:14:22] [Proxy] ACCESS GRANTED — James Nghipandua accessed [Plumbing] #d7e02b — Replace burst pipe (Active).
  [09:14:22] [Proxy] ACCESS GRANTED — James Nghipandua accessed [Carpentry] #c2f89d — Fit new cabinet frames (Active).

=== Uncertified Handyman Access ===
  [09:14:22] [Proxy] ACCESS GRANTED — Tom Kamati accessed [Carpentry] #c2f89d — Fit new cabinet frames (Active).
  [09:14:22] [Proxy] ACCESS DENIED — Tom Kamati lacks Electrical certification for Job #a3f12b.
  ❌ Caught: Tom Kamati is not certified for Electrical jobs.

=== Virtual Proxy (Lazy Loading) ===
  LazyRegistry created — no real registry yet.
  First access triggers initialisation...
  [LazyProxy] Initialising real registry (first access)...
  [Registry] Registered: [General] #e9b14c — Paint exterior walls (Active).

TypeScript Implementation

// ── Subject/IJobCardAccess.ts ──────────────────────────────

import { JobCard } from "../Model/JobCard";

export interface IJobCardAccess {
  register(card: JobCard): void;
  getJobCard(jobId: string): JobCard | undefined;
  getAll(): ReadonlyArray<JobCard>;
}

// ── Model/JobCard.ts ───────────────────────────────────────

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

export class JobCard {
  readonly jobId: string = shortId();
  status: string = "Active";

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

  toString(): string {
    return `[${this.category}] #${this.jobId} — ${this.title} (${this.status})`;
  }
}

// ── Real/JobCardRegistry.ts ────────────────────────────────

import { IJobCardAccess } from "../Subject/IJobCardAccess";
import { JobCard        } from "../Model/JobCard";

export class JobCardRegistry implements IJobCardAccess {
  private store = new Map<string, JobCard>();

  register(card: JobCard): void {
    this.store.set(card.jobId, card);
    console.log(`  [Registry] Registered: ${card}`);
  }

  getJobCard(jobId: string): JobCard | undefined {
    return this.store.get(jobId);
  }

  getAll(): ReadonlyArray<JobCard> {
    return Array.from(this.store.values());
  }
}

// ── Model/Handyman.ts ──────────────────────────────────────

export class Handyman {
  constructor(
    readonly name:           string,
    readonly certifications: string[]
  ) {}

  hasCertification(category: string): boolean {
    return this.certifications
      .some(c => c.toLowerCase() === category.toLowerCase());
  }
}

// ── Proxy/JobCardAccessProxy.ts ────────────────────────────

import { IJobCardAccess } from "../Subject/IJobCardAccess";
import { JobCard        } from "../Model/JobCard";
import { Handyman       } from "../Model/Handyman";

export class JobCardAccessProxy implements IJobCardAccess {
  private static readonly RESTRICTED = new Set(["electrical", "plumbing"]);

  constructor(
    private readonly registry: IJobCardAccess,
    private readonly handyman: Handyman
  ) {}

  register(card: JobCard): void {
    this.log(`REGISTER — ${card}`);
    this.registry.register(card);
  }

  getJobCard(jobId: string): JobCard | undefined {
    const card = this.registry.getJobCard(jobId);

    if (!card) {
      this.log(`GET #${jobId} — not found.`);
      return undefined;
    }

    if (JobCardAccessProxy.RESTRICTED.has(card.category.toLowerCase()) &&
        !this.handyman.hasCertification(card.category)) {
      this.log(`ACCESS DENIED — ${this.handyman.name} lacks ${card.category} certification for Job #${jobId}.`);
      throw new Error(`${this.handyman.name} is not certified for ${card.category} jobs.`);
    }

    this.log(`ACCESS GRANTED — ${this.handyman.name} accessed ${card}.`);
    return card;
  }

  getAll(): ReadonlyArray<JobCard> {
    this.log(`GET ALL — ${this.handyman.name} listed all job cards.`);
    return this.registry.getAll();
  }

  private log(message: string): void {
    const time = new Date().toTimeString().slice(0, 8);
    console.log(`  [${time}] [Proxy] ${message}`);
  }
}

// ── Proxy/LazyJobCardRegistry.ts ──────────────────────────

import { IJobCardAccess  } from "../Subject/IJobCardAccess";
import { JobCard         } from "../Model/JobCard";
import { JobCardRegistry } from "../Real/JobCardRegistry";

export class LazyJobCardRegistry implements IJobCardAccess {
  private realRegistry?: JobCardRegistry;

  private get registry(): JobCardRegistry {
    if (!this.realRegistry) {
      console.log("  [LazyProxy] Initialising real registry (first access)...");
      this.realRegistry = new JobCardRegistry();
    }
    return this.realRegistry;
  }

  register(card: JobCard): void       { this.registry.register(card); }
  getJobCard(jobId: string)           { return this.registry.getJobCard(jobId); }
  getAll(): ReadonlyArray<JobCard>    { return this.registry.getAll(); }
}

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

import { JobCard             } from "./Model/JobCard";
import { Handyman            } from "./Model/Handyman";
import { JobCardRegistry     } from "./Real/JobCardRegistry";
import { JobCardAccessProxy  } from "./Proxy/JobCardAccessProxy";
import { LazyJobCardRegistry } from "./Proxy/LazyJobCardRegistry";

// ── Setup ──────────────────────────────────────────────────
const realRegistry   = new JobCardRegistry();
const plumbingJob    = new JobCard("Replace burst pipe",      "Plumbing");
const electricalJob  = new JobCard("Install circuit breaker", "Electrical");
const carpentryJob   = new JobCard("Fit new cabinet frames",  "Carpentry");

realRegistry.register(plumbingJob);
realRegistry.register(electricalJob);
realRegistry.register(carpentryJob);

// ── Certified handyman ─────────────────────────────────────
const certifiedHandyman = new Handyman("James Nghipandua", ["Electrical", "Plumbing"]);
const certifiedProxy    = new JobCardAccessProxy(realRegistry, certifiedHandyman);

console.log("\n=== Certified Handyman Access ===");
certifiedProxy.getJobCard(electricalJob.jobId);
certifiedProxy.getJobCard(plumbingJob.jobId);
certifiedProxy.getJobCard(carpentryJob.jobId);

// ── Uncertified handyman ───────────────────────────────────
const uncertifiedHandyman = new Handyman("Tom Kamati", ["Carpentry"]);
const uncertifiedProxy    = new JobCardAccessProxy(realRegistry, uncertifiedHandyman);

console.log("\n=== Uncertified Handyman Access ===");
uncertifiedProxy.getJobCard(carpentryJob.jobId);

try {
  uncertifiedProxy.getJobCard(electricalJob.jobId);
} catch (e) {
  console.log(`  ❌ Caught: ${(e as Error).message}`);
}

// ── Virtual proxy ──────────────────────────────────────────
console.log("\n=== Virtual Proxy (Lazy Loading) ===");
console.log("  LazyRegistry created — no real registry yet.");

const lazyRegistry = new LazyJobCardRegistry();
console.log("  First access triggers initialisation...");
lazyRegistry.register(new JobCard("Paint exterior walls", "General"));

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Access denied throw new UnauthorizedAccessException(...) throw new Error(...)
Lazy initialisation _realRegistry ??= CreateRegistry() Private getter with if (!this.realRegistry)
Null coalescing assign ??= operator No direct equivalent — use if check
Case-insensitive set new HashSet<string>(StringComparer.OrdinalIgnoreCase) category.toLowerCase() before Set.has()
Interface return types IReadOnlyList<JobCard> ReadonlyArray<JobCard>
Timestamp DateTime.Now.ToString("HH:mm:ss") new Date().toTimeString().slice(0, 8)

The ??= null coalescing assignment operator in C# is particularly elegant for lazy initialisation — one of those small quality-of-life features TypeScript doesn't have a direct equivalent for.


Proxy vs Decorator — What's the Difference?

They look almost identical in code. The distinction is intent:

Proxy Decorator
Intent Control access to the real object Add behaviour to the real object
Knows about subject Often creates or manages the real subject Never creates — always receives it
Client awareness Client doesn't know it's talking to a proxy Client may explicitly stack decorators
FixIt Pro Access control + logging + lazy load Urgent, Insured, Warrantied extras
Relationship Proxy IS a stand-in Decorator IS an enhancement

The rule of thumb: if you're controlling access, it's a Proxy. If you're adding behaviour, it's a Decorator.


When to Use the Proxy

Use it when:

  • You need access control (Protection Proxy)
  • You need to log or audit calls transparently (Logging Proxy)
  • Object creation is expensive and should be deferred (Virtual Proxy)
  • You need a local representative of a remote object (Remote Proxy)
  • You want to add reference counting or lifecycle management (Smart Reference Proxy)

Avoid it when:

  • The extra indirection adds complexity without measurable benefit
  • You control the real subject and can add the behaviour directly

Real-World Takeaway

The Proxy is everywhere in modern software. In .NET, Entity Framework Core uses a Virtual Proxy for lazy-loaded navigation properties — the related entity isn't fetched from the database until you access the property. ASP.NET Core's authentication middleware is a Protection Proxy — requests are checked before reaching the controller. In JavaScript, the native Proxy object is a language-level implementation of this exact pattern — it wraps any object and intercepts get, set, and delete operations. In NestJS, Guards and Interceptors are proxy-style mechanisms applied at the request level.

In FixIt Pro, Tom Kamati will never open an electrical job card without certification. Every access is logged with a timestamp. And expensive registry initialisation only happens when someone actually needs it. Three proxy types, one interface, zero changes to the real registry.


Wrapping Up Structural Patterns

This is Episode 12 — the final Structural pattern. Here's the complete Structural block:

# Pattern FixIt Pro scenario
06 Adapter Legacy SMS gateway → modern INotifier
07 Bridge Urgency levels × notification channels
08 Composite Compound job trees treated uniformly
09 Decorator Urgent, Insured, Warrantied stacked at runtime
10 Facade Five subsystems behind one DispatchJob() call
11 Flyweight 100,000 job cards, 3 shared metadata objects
12 Proxy Access control, logging, and lazy loading

Next up: Behavioral patterns — how objects communicate and distribute responsibility. Episode 13 opens with the Chain of Responsibility pattern.


Repo Structure for This Episode

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

fixit-pro-design-patterns/
├── csharp/Structural/12-Proxy/
│   ├── Subject/IJobCardAccess.cs
│   ├── Model/JobCard.cs
│   ├── Model/Handyman.cs
│   ├── Real/JobCardRegistry.cs
│   ├── Proxy/JobCardAccessProxy.cs
│   ├── Proxy/LazyJobCardRegistry.cs
│   └── Program.cs
└── typescript/Structural/12-Proxy/
    ├── Subject/IJobCardAccess.ts
    ├── Model/JobCard.ts
    ├── Model/Handyman.ts
    ├── Real/JobCardRegistry.ts
    ├── Proxy/JobCardAccessProxy.ts
    ├── Proxy/LazyJobCardRegistry.ts
    └── App.ts

Previous: #11 — Flyweight Pattern
Next up: #13 — Chain of Responsibility Pattern A job approval chain: Handyman → Supervisor → Manager, based on cost threshold. Each handler decides whether to handle the request or pass it up the chain.