Skip to main content

Design Patterns in Real Code: Factory, Strategy, Decorator – C# | Lesson 56

Published / updated

Introduction​

A design pattern is a named, proven solution to a problem that keeps coming up in software design. Patterns are not libraries you install; they are shapes of code that experienced developers recognise, so "use a decorator here" says a whole design in two words.

You have already written several without their names: the template method in Lesson 40, events in Lesson 51, a strategy with Func in Lesson 51. This lesson names five patterns and shows each one in SchoolApp code.

By the end of this lesson, you will be able to:

  • Explain the Factory, Strategy, Decorator, Observer, and Singleton patterns.
  • Recognise each one in SchoolApp code.
  • Implement a strategy and a decorator behind an interface.
  • Explain why dependency injection usually replaces the classic Singleton.
  • Judge when a pattern helps, and when it only adds layers.

Why this matters​

"Which design patterns have you used?" is a standard question from the second interview round onward. A list of names is a weak answer. "We chose the sender with a factory, so adding WhatsApp touched one class" is a strong one.

Patterns also help you read frameworks. ASP.NET Core's middleware is a chain of decorators; its dependency injection replaces singletons and factories; its logging uses strategies. Module 10 starts using all of them.

The concept​

Example: five patterns at a glance​

PatternProblem it solvesIn SchoolApp
FactoryChoosing which class to create is spread all over the codeNotificationSenderFactory.For(guardian)
StrategyOne task, several interchangeable rulesIGradeScale: primary bands or A1 to E
DecoratorAdd behaviour, such as logging, without changing a classLoggingSender wraps any sender
ObserverMany parts must react when something happensFeeInvoice.PaymentRecorded (Lesson 51)
SingletonExactly one shared instance is neededSchoolSettings.Instance

These five come from a famous 1994 book often called the "Gang of Four" book, which described 23 patterns. You do not need all 23; these five appear in nearly every C# codebase.

Example: Factory​

A factory is a method or class whose only job is to decide which object to create. Before it, every place that sent a notice repeated guardian.Email is null ? new PhoneSender() : new EmailSender().

Think of the school's front office. A visitor says "I need to speak to someone about fees", and the office decides whether that is the accountant or the principal. The visitor does not need to know the staff list.

Example: Strategy​

A strategy is a family of interchangeable rules behind one interface. The code that uses the rule does not change when the rule does. IGradeScale has two strategies: descriptive bands for classes 1 to 5, and the A1 to E scale for classes 6 to 10.

Lesson 51 did the same with a Func<decimal, string>. A delegate suits a one-method rule; an interface suits a rule with several members, such as a name and a grade.

Example: Decorator​

A decorator implements an interface and holds another object of the same interface. Each call does something extra, such as logging, then passes the call on to the wrapped object.

Think of a gift wrapped in paper: still the same gift, with something added around it. Decorators can be stacked: logging around retrying around the real sender, each unaware of the others.

Apply it to the SMS​

Step 1: the factory and the decorator​

src/SchoolApp.Core/Notifications/NotificationSenderFactory.cs
using SchoolApp.Core.Entities;
using SchoolApp.Core.Interfaces;

namespace SchoolApp.Core.Notifications;

// Lesson 56, Factory pattern: one place decides which kind of sender a guardian needs.
// Callers ask for "a sender for this guardian" and never write new EmailSender() themselves.
public static class NotificationSenderFactory
{
public static INotificationSender For(Guardian guardian)
{
return string.IsNullOrWhiteSpace(guardian.Email) ? new PhoneSender() : new EmailSender();
}
}
src/SchoolApp.Core/Notifications/LoggingSender.cs
using SchoolApp.Core.Entities;
using SchoolApp.Core.Interfaces;

namespace SchoolApp.Core.Notifications;

// Lesson 56, Decorator pattern: wraps any sender and adds logging, without changing the sender.
// The log is a delegate, so Core never writes to the console itself.
public sealed class LoggingSender : INotificationSender
{
private readonly INotificationSender _inner;
private readonly Action<string> _log;

public LoggingSender(INotificationSender inner, Action<string> log)
{
_inner = inner;
_log = log;
}

public string Channel => _inner.Channel;

public bool CanReach(Guardian guardian) => _inner.CanReach(guardian);

public string Send(Guardian guardian, string message)
{
_log($"Sending by {Channel} to {guardian.FullName}");
string result = _inner.Send(guardian, message);
_log($"Done: {Channel}");
return result;
}
}

LoggingSender takes its log as an Action<string> delegate. Core still never writes to the console itself; the caller decides where log lines go, as the Web API will do with its logger.

Step 2: the grade scale strategies​

src/SchoolApp.Core/Rules/GradeScales.cs
namespace SchoolApp.Core.Rules;

// Lesson 56, Strategy pattern: interchangeable grading rules behind one interface.
public interface IGradeScale
{
string Name { get; }
string Grade(decimal percentage);
}

// Classes 6 to 10: the A1 to E scale of fn_GetGrade, reused from SchoolRules.
public sealed class SecondaryGradeScale : IGradeScale
{
public string Name => "Secondary (A1 to E)";
public string Grade(decimal percentage) => SchoolRules.GetGrade(percentage);
}

// Classes 1 to 5: descriptive bands, as on the primary report card (Lesson 41).
public sealed class PrimaryGradeScale : IGradeScale
{
public string Name => "Primary (bands)";
public string Grade(decimal percentage) => percentage switch
{
>= 81 => "Excellent",
>= 51 => "Good",
_ => "Needs support"
};
}

public static class GradeScales
{
// A small factory again: the class number picks the strategy.
public static IGradeScale ForClass(int classNumber) =>
classNumber <= 5 ? new PrimaryGradeScale() : new SecondaryGradeScale();
}

Step 3: the program​

Show src/SchoolApp.Console/Program.cs
src/SchoolApp.Console/Program.cs
// Lesson 56: design patterns in SchoolApp.
using SchoolApp.Core.Entities;
using SchoolApp.Core.Interfaces;
using SchoolApp.Core.Notifications;
using SchoolApp.Core.Rules;

Guardian rajesh = new Guardian("Rajesh", "Sharma", "9876540101") { Email = "rajesh.sharma@nexcodingschool.example" };
Guardian suresh = new Guardian("Suresh", "Patel", "9876540103");

// 1. Factory: the factory chooses the sender; the caller only asks.
foreach (Guardian guardian in new[] { rajesh, suresh })
{
INotificationSender sender = NotificationSenderFactory.For(guardian);
Console.WriteLine($"{guardian.FullName}: {sender.Channel}");
}

// 2. Decorator: add logging around any sender, without changing the sender.
INotificationSender logged = new LoggingSender(NotificationSenderFactory.For(suresh), line => Console.WriteLine($" [log] {line}"));
Console.WriteLine(logged.Send(suresh, "Diya: fee balance 25500. NexCoding Public School"));

// 3. Strategy: the same report code with a grading rule chosen per class.
PrintGrades("Aditya Kapoor", classNumber: 3, [("Science", 98m), ("Mathematics", 69m), ("Computer Science", 23m)]);
PrintGrades("Meera Iyer", classNumber: 10, [("Mathematics", 95m)]);

// 4. Observer: FeeInvoice announces payments; observers subscribe (Lesson 51).
FeeInvoice invoice = new FeeInvoice("INV-2026-000002", 45500m);
invoice.PaymentRecorded += (_, e) => Console.WriteLine($"Observer: {e.InvoiceNumber} is {e.Status}, balance {e.Balance}");
invoice.RecordPayment(20000m);

// 5. Singleton: one shared instance for the whole program.
Console.WriteLine($"Same settings object: {ReferenceEquals(SchoolSettings.Instance, SchoolSettings.Instance)}; school: {SchoolSettings.Instance.SchoolName}");

static void PrintGrades(string student, int classNumber, (string Subject, decimal Percentage)[] results)
{
IGradeScale scale = GradeScales.ForClass(classNumber);
string grades = string.Join(", ", results.Select(r => $"{r.Subject} {scale.Grade(r.Percentage)}"));
Console.WriteLine($"{student}, Class {classNumber}, {scale.Name}: {grades}");
}

// The classic singleton: a private constructor and one lazily created instance.
// In the Web API, dependency injection registers a singleton instead (Module 10).
sealed class SchoolSettings
{
private static readonly Lazy<SchoolSettings> _instance = new(() => new SchoolSettings());

public static SchoolSettings Instance => _instance.Value;

public string SchoolName { get; } = "NexCoding Public School";

private SchoolSettings()
{
}
}
Output
Rajesh Sharma: Email
Suresh Patel: Phone
[log] Sending by Phone to Suresh Patel
[log] Done: Phone
[Text to 9876540103] Diya: fee balance 25500. NexCoding Public School
Aditya Kapoor, Class 3, Primary (bands): Science Excellent, Mathematics Good, Computer Science Needs support
Meera Iyer, Class 10, Secondary (A1 to E): Mathematics A1
Observer: INV-2026-000002 is PartiallyPaid, balance 25500
Same settings object: True; school: NexCoding Public School

Example: the factory in action​

The factory returned an email sender for Rajesh, the only guardian with an email, and a phone sender for Suresh. If the school adds WhatsApp as a channel next year, the rule changes in one method, and every caller of For gets the new behaviour.

Example: the decorator in action​

The phone sender was wrapped in LoggingSender, and the two log lines appeared around the real send. The phone sender itself did not change, and code holding an INotificationSender cannot tell that it is wrapped.

This is also the Open/Closed Principle of Lesson 46: logging was added by writing a new class, not by editing either sender. ASP.NET Core's middleware pipeline in Module 10 works the same way.

Example: the strategy in action​

PrintGrades is written once. For Aditya in Class 3, GradeScales.ForClass(3) returned the primary bands: his real marks of 98, 69, and 23 became Excellent, Good, and Needs support. For Meera in Class 10, the secondary scale turned 95 into A1.

PrintGrades receives the results as an array of tuples: (string Subject, decimal Percentage) groups two values without declaring a type. Tuples suit quick, local groupings like this; records are better for anything shared between classes.

Example: Singleton and why DI replaces it​

SchoolSettings has a private constructor and one static instance, created on first use with Lazy<T>. Both calls returned the same object.

The classic singleton has real costs. Any code can reach it, so dependencies are hidden, and tests cannot replace it with a fake.

Its shared state also has every threading problem from Lesson 54.

In ASP.NET Core, you register a class as a singleton service with dependency injection instead: one instance, but passed in through constructors, visible, and replaceable in tests. That is the modern form of this pattern, and Module 10 uses it.

Example: when not to use a pattern​

SignProbably over-engineered
A factory that always returns the same classWrite new directly
A strategy interface with one implementation and no plan for moreUse a plain method
Decorators around code that nobody needs to extendAdd the behaviour directly
A singleton for something that could be a constantUse const or configuration

A pattern should remove a real cost: repeated decisions, changing rules, or behaviour that must be added without touching tested code. If you cannot name that cost, the pattern only adds files.

Common mistakes​

  1. Naming patterns without understanding the problem. Fix: for each pattern, know the problem it solves and one real example.
  2. Classic singletons with mutable state. Hidden dependencies and thread-safety bugs. Fix: dependency injection with a singleton lifetime, and immutable data.
  3. Forgetting to pass calls through in a decorator. The wrapped object never runs. Fix: every member calls the inner object.
  4. new scattered across the code for classes that vary. Adding a variant means many edits. Fix: a factory.
  5. Patterns everywhere. Layers with no benefit. Fix: apply a pattern when its cost is clearly lower than the problem it removes.

In real projects​

From 16 years of .NET projects

On one SMS, sending through a new text-message provider needed retries and logging. The first proposal copied the provider class and added both features inside, duplicating the sending code.

We used two decorators instead, a retrying one and a logging one, wrapped around the unchanged provider class. Later, a second provider got both features for free by being wrapped the same way.

The best pattern discussions I have had were short: "this is a decorator" settled the design in a sentence. That is what the names are for.

Best practices​

  • Learn patterns through the problems they solve, not their diagrams.
  • Use a factory where the choice of class varies; a strategy where the rule varies.
  • Add cross-cutting behaviour, such as logging and retries, with decorators.
  • Prefer dependency injection with a singleton lifetime over the classic Singleton.
  • Name the pattern in a code comment or review when it helps others understand the design.

Recap​

  • A design pattern is a named, proven solution to a recurring design problem.
  • Factory: one place chooses the class, such as email or phone sender.
  • Strategy: interchangeable rules behind one interface, such as primary or secondary grades.
  • Decorator: wraps an object to add behaviour, such as logging, without changing it.
  • Observer is the event pattern of Lesson 51; Singleton is better done with dependency injection.
  • Use a pattern only when you can name the cost it removes.

Homework​

Library module task: Calculate library fines with three design patterns.

  1. Strategy: write IFinePolicy with a student policy (2 per day) and a staff policy (5 per day).
  2. Factory: write a factory that picks the policy by borrower type.
  3. Decorator: write CappedFine, which limits any policy to a maximum. For this exercise, the maximum is 50.

Show the fines for 8 and 30 days late, for both kinds of borrower, with and without the cap.

A correct result looks like:

Output
student: 8 days late -> 16; 30 days late -> 60, capped 50
staff : 8 days late -> 40; 30 days late -> 150, capped 50
Show the solution
library-assignment/056-library-patterns.cs
// Library homework, Lesson 56: fine policies as strategies, chosen by a factory, wrapped by a decorator.
// Run with: dotnet run library-assignment/056-library-patterns.cs
foreach (string borrower in new[] { "student", "staff" })
{
IFinePolicy policy = FinePolicies.For(borrower);
IFinePolicy capped = new CappedFine(policy, maximum: 50m);
Console.WriteLine($"{borrower,-7}: 8 days late -> {policy.Fine(8),3}; 30 days late -> {policy.Fine(30),3}, capped {capped.Fine(30)}");
}

// Strategy: interchangeable fine rules.
interface IFinePolicy
{
decimal Fine(int daysLate);
}

sealed class StudentFine : IFinePolicy
{
public decimal Fine(int daysLate) => Math.Max(0, daysLate) * 2m;
}

sealed class StaffFine : IFinePolicy
{
public decimal Fine(int daysLate) => Math.Max(0, daysLate) * 5m;
}

// Decorator: adds a maximum to any policy, without changing it.
sealed class CappedFine(IFinePolicy inner, decimal maximum) : IFinePolicy
{
public decimal Fine(int daysLate) => Math.Min(inner.Fine(daysLate), maximum);
}

// Factory: one place that knows which policy applies.
static class FinePolicies
{
public static IFinePolicy For(string borrowerType) => borrowerType switch
{
"staff" => new StaffFine(),
_ => new StudentFine()
};
}

CappedFine uses a primary constructor (Lesson 52) and wraps any IFinePolicy, so the cap works for both policies, and for any policy added later, without changing them.

Quiz​

  1. Which pattern decides which class to create in one place?

    1. Strategy
    2. Factory
    3. Decorator
    4. Observer
    Show answer

    Answer: B. Factory NotificationSenderFactory chooses email or phone.

  2. Primary bands or the A1 to E scale, chosen by class: which pattern?

    1. Singleton
    2. Decorator
    3. Strategy
    4. Factory only
    Show answer

    Answer: C. Strategy Interchangeable rules behind IGradeScale.

  3. LoggingSender implements INotificationSender and wraps another sender. Which pattern?

    1. Decorator
    2. Observer
    3. Singleton
    4. Template method
    Show answer

    Answer: A. Decorator It adds logging around the wrapped object without changing it.

  4. Which pattern do C# events implement?

    1. Factory
    2. Observer
    3. Strategy
    4. Builder
    Show answer

    Answer: B. Observer Subscribers are notified when the subject raises the event.

  5. Why prefer dependency injection over a classic Singleton?

    1. Singletons are slow
    2. Dependencies become visible and replaceable in tests
    3. DI creates more instances
    4. Singletons cannot hold data
    Show answer

    Answer: B. Dependencies become visible and replaceable in tests A DI singleton is still one instance, but passed in, not reached for.

Interview questions​

Questions are grouped by experience level. Click a question to see a model answer. All questions: Architecture interview questions.

Fresher (0-2 yrs)​

F What is a design pattern? concept

A named, proven solution to a recurring design problem, described as a shape of code rather than a library. Names such as Factory or Decorator let developers communicate a whole design in a word.

See the example · Permalink

F What is the Factory pattern? Give an example. concept

A method or class whose job is to decide which object to create, so the decision lives in one place. NotificationSenderFactory.For(guardian) returns an email or phone sender; callers never write new EmailSender() themselves.

See the example · Permalink

F What is the Strategy pattern? concept

A family of interchangeable rules behind one interface, chosen at run time. IGradeScale has a primary-band strategy and an A1-to-E strategy; the report code uses whichever it is given.

See the example · Permalink

Mid (3-5 yrs)​

M What is the Decorator pattern, and where does .NET use it? concept

A class that implements an interface and wraps another object of the same interface, adding behavior before or after passing calls on, like LoggingSender. ASP.NET Core middleware, stream wrappers, and retry or caching layers follow the same idea.

See the example · Permalink

M What is the Observer pattern, and how does C# support it? concept

A subject notifies many observers when something happens, without knowing them. C# events implement it directly: FeeInvoice.PaymentRecorded notifies every subscriber after a payment.

See the example · Permalink

M What are the problems with the classic Singleton pattern? concept

Any code can reach it through a static property, so dependencies are hidden; tests cannot replace it; and its shared state must be thread-safe. Registering the class as a singleton service with dependency injection keeps one instance while making it explicit and replaceable.

See the example · Permalink

Senior (6-9 yrs)​

S A text-message provider needs retries and logging. A colleague proposes copying the provider class and adding both. What do you suggest? scenario

Use decorators: a retrying wrapper and a logging wrapper around the unchanged provider, all implementing the same interface. Each concern stays separate and testable, and any future provider gets both by being wrapped the same way.

How to answer: Name the pattern, explain why copying duplicates and couples concerns, and mention ordering of decorators (logging outside retrying shows each attempt or not) and DI registration.

See the example · Permalink

S When is using a design pattern over-engineering? design

When it removes no real cost: a factory that always returns one class, a strategy interface with one implementation and no plan for more, decorators no one needs. Apply a pattern when you can name the repeated decision, changing rule, or extension it supports.

How to answer: Give concrete signs of over-engineering and the cost (more files, indirection), then a rule for deciding, such as introducing the pattern when the second variant appears.

See the example · Permalink

Architect / Lead (10+ yrs)​

A Which patterns does ASP.NET Core rely on, and how does that shape application design? design

Dependency injection (replacing factories and singletons), decorators in the middleware pipeline, strategies for logging and configuration providers, and options and observers for configuration changes. Applications should depend on interfaces, register implementations in one place, and add cross-cutting concerns as middleware or decorators.

How to answer: Connect each framework feature to its pattern, then explain the consequences for structuring SMS code: thin controllers, services behind interfaces, cross-cutting concerns outside business logic.

See the example · Permalink

Resources​

  • Code for this lesson: git tag lesson-56-design-patterns (code repository coming soon)
  • Files: src/SchoolApp.Core/Notifications/NotificationSenderFactory.cs, src/SchoolApp.Core/Notifications/LoggingSender.cs, src/SchoolApp.Core/Rules/GradeScales.cs, src/SchoolApp.Console/Program.cs, library-assignment/056-library-patterns.cs
  • Video: coming soon

What's next​

Module 8 is complete. SchoolApp has a full object model, collections, LINQ, async code, and a set of well-known patterns.

Module 9 connects it to SchoolDb: ADO.NET, Dapper, and EF Core, behind the repository interface from Lesson 46. Lesson 57: ADO.NET

Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.