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.