SOLID Principles & Design Patterns: A Game of Thrones Developer's Guide
When you play the game of thrones, you win or you die. When you play the game of software, you either write clean code or you maintain spaghetti forever. — Cersei Lannister (probably, if she were a tech lead)

"Uy, natuto ka na ba ng SOLID?" (Hey, have you learned SOLID yet?)
Every senior developer I've worked with has asked me this at some point — usually right after I showed them a 400-line UserService that handled authentication, email sending, PDF generation, and somehow also computed the employee's age. My code was like the Small Council: everyone doing everything and nobody accountable for anything.
So let's fix that. And since we're all still emotionally damaged from that last season, we might as well make it useful.
Buckle up. We're going to Westeros.
🏰 S — Single Responsibility Principle
"A man is no one. But a class should be someone very specific."
The rule: A class should have only one reason to change.
Cersei Lannister tried to be Queen, spy master, military strategist, wine sommelier, and loving mother all at once. We all saw how that ended. Burnt. Literally.
Here's the classic mistake I see in .NET projects all the time:
// ❌ The Cersei class — does EVERYTHING and trusts NO ONE
public class KingdomService
{
public void CrownTheKing(string name) { /* ... */ }
public void SendRavenMessage(string message) { /* ... */ }
public void CalculateTaxes(int gold) { /* ... */ }
public void ExecutePrisoner(string prisonerName) { /* ... */ }
public void BakeWeddingCake(string flavor) { /* ... */ }
}
Grabe. (Good grief.) This thing has more responsibilities than Tyrion at a family dinner.
// ✅ The Small Council approach — each house handles their own domain
public class CoronationService
{
public void CrownTheKing(string name) { /* ... */ }
}
public class RavenMessagingService
{
public void SendMessage(string message, string recipient) { /* ... */ }
}
public class TreasuryService
{
public decimal CalculateTaxes(int goldDragons) { /* ... */ }
}
Each class has one job. When ravens stop working, you know exactly which class to fix. When the Treasury explodes (and it will), you don't have to untangle it from the coronation logic.
Real talk: I once had a StudentService in a school management system that was handling enrollment, billing, grade computation, and generating DepEd Form 137. When the BIR compliance format changed, I had to touch a class that was also responsible for academic records. Sobrang hirap. (Absolutely brutal.)
🔓 O — Open/Closed Principle
"The Iron Throne was built to be sat on. Not rewritten every time a new king arrives."
The rule: Software entities should be open for extension but closed for modification.
Every time a new Targaryen shows up, do you rebuild King's Landing? No. You extend the city. Add new districts. Same idea.
// ❌ Modifying the base every time a new house joins the war
public class BattleCalculator
{
public int CalculatePower(string houseName)
{
if (houseName == "Stark") return 100 + GetNorthernBonus();
if (houseName == "Lannister") return 100 + GetGoldBonus();
if (houseName == "Targaryen") return 100 + GetDragonBonus(); // Added later
if (houseName == "Greyjoy") return 100 + GetSeaBonus(); // Added even later
// ...and so it grows until someone quits
return 100;
}
}
Every new house means touching production code. Every touch is a risk. Every risk is a potential Purple Wedding.
// ✅ Open for extension, closed for modification
public abstract class GreatHouse
{
public abstract string Name { get; }
public abstract int GetPowerBonus();
public int TotalPower() => 100 + GetPowerBonus();
}
public class HouseStark : GreatHouse
{
public override string Name => "Stark";
public override int GetPowerBonus() => GetNorthernBonus();
private int GetNorthernBonus() => 40; // Winter is coming, and it empowers them
}
public class HouseTargaryen : GreatHouse
{
public override string Name => "Targaryen";
public override int GetPowerBonus() => GetDragonBonus();
private int GetDragonBonus() => IsDragonAlive() ? 999 : -50;
private bool IsDragonAlive() => true; // RIP Viserion :(
}
// New house? Just extend. Zero modifications to existing code.
public class HouseMartell : GreatHouse
{
public override string Name => "Martell";
public override int GetPowerBonus() => 35; // Unbowed. Unbent. Unbroken.
}
Real talk: I applied this in our Azure Integration Services pipeline. Every new message type used to mean touching the core dispatcher. After refactoring with the strategy pattern + OCP, adding a new message handler was just adding a new class and registering it. The dispatcher never changed. My lead review went from "10 comments" to "LGTM." Finally. Finally.
🔄 L — Liskov Substitution Principle
"Any Stark should be able to do what a Stark does. The moment your subclass betrays its parent's contract, you've got a Theon situation."
The rule: Subtypes must be substitutable for their base types without breaking the program.
Poor Theon. He was supposed to be a Stark ward but he acted Greyjoy when it mattered most. LSP violation: classic.
// The base contract
public abstract class Dragon
{
public abstract void Fly();
public abstract void BreatheFire();
}
// ✅ Fully substitutable
public class Drogon : Dragon
{
public override void Fly() => Console.WriteLine("Soaring over King's Landing...");
public override void BreatheFire() => Console.WriteLine("🔥 DRACARYS 🔥");
}
// ❌ LSP Violation — the classic "but I don't support that" subclass
public class DragonEgg : Dragon
{
public override void Fly()
=> throw new NotImplementedException("I'm literally an egg. I can't fly."); // 💀
public override void BreatheFire()
=> throw new NotImplementedException("ALSO AN EGG.");
}
If your code has if (dragon is DragonEgg) skip the flying part — you've broken LSP. The moment calling code needs to know the concrete type, your abstraction is lying to you.
// ✅ The correct design
public abstract class Dragon
{
public abstract void BreatheFire();
}
public abstract class FlyingCreature
{
public abstract void Fly();
}
public class Drogon : Dragon, FlyingCreature
{
public override void BreatheFire() => Console.WriteLine("DRACARYS");
public override void Fly() => Console.WriteLine("Cleared the Dothraki Sea in 3 minutes");
}
public class DragonEgg // Not a Dragon yet. It's an egg. Stop lying.
{
public Dragon Hatch() => new Drogon();
}
Real talk: I once saw a PremiumUser : User class that threw UnauthorizedException on GetBasicProfile() because "premium users use a different endpoint." My senior devs face when I showed him: 😑. We refactored it that sprint. Some lessons you only need once.
🧩 I — Interface Segregation Principle
"You wouldn't ask a maester to swing a sword. Don't force your classes to implement methods they'll never use."
The rule: Clients should not be forced to depend on interfaces they don't use.
The Night King only needed to raise the dead and stare menacingly. Imagine if he were forced to implement ITournamentParticipant, IRoyalCourt, and IWeddingPlanner just because the base interface demanded it. Nakakabaliw. (Absolutely maddening.)
// ❌ The Fat Interface — the Night King dilemma
public interface IWesterosCitizen
{
void PayTaxes();
void AttendTournament();
void SwearFealty(string lordName);
void RaiseTheUndead(); // Most citizens: ???
void WieldValyrianSteel(); // Also most citizens: ???
void PlanAWedding(); // Night King: hard pass
}
Now every class that implements IWesterosCitizen has to stub out RaiseTheUndead() with a NotImplementedException or a sarcastic throw new YouAreNotTheNightKingException().
// ✅ Segregated interfaces — each lord handles their own domain
public interface ITaxpayer
{
void PayTaxes();
}
public interface ITournamentParticipant
{
void AttendTournament();
void JoustAgainst(string opponent);
}
public interface INecromancer
{
void RaiseTheUndead(IEnumerable<Corpse> fallen);
}
// Now each class only implements what it actually does
public class NightKing : INecromancer
{
public void RaiseTheUndead(IEnumerable<Corpse> fallen)
{
foreach (var corpse in fallen)
corpse.Rise(TeamUndead);
}
// No forced tournament attendance. No taxes. Iconic.
}
public class SerBrienne : ITournamentParticipant, ITaxpayer
{
public void AttendTournament() => Console.WriteLine("Wins in the first round, as usual.");
public void JoustAgainst(string opponent) => Console.WriteLine($"Defeats {opponent} honorably.");
public void PayTaxes() => Console.WriteLine("Pays in full. Brienne doesn't skip taxes.");
}
Real talk: We had a massive IMessageProcessor interface in one of our Azure Service Bus handlers. Every new handler had to implement 9 methods even if it only used 2. After splitting into IMessageDeserializer, IMessageValidator, and IMessageRouter, each handler class dropped from ~200 lines to ~40. Napakagaan. (So much lighter.)
🔀 D — Dependency Inversion Principle
"High lords should not depend on peasants. Both should depend on the law of the realm."
The rule: High-level modules should not depend on low-level modules. Both should depend on abstractions.
If Jon Snow personally visited every house to gather battle intelligence instead of going through the ravens (the abstraction), he'd be exhausted and tightly coupled to every maester in Westeros. Change one maester, Jon breaks. Classic tight coupling.
// ❌ Jon Snow doing everything manually — tightly coupled
public class BattleCommandService
{
private readonly WinterfellRavenMaester _ravenMaester = new WinterfellRavenMaester();
private readonly SqlServerBattleLog _battleLog = new SqlServerBattleLog();
public void CommandAttack(string target)
{
_ravenMaester.SendRaven($"Attack {target}"); // Hardcoded. Brittle.
_battleLog.LogBattle(target, DateTime.UtcNow); // SqlServer forever.
}
}
What if you want to switch from SQL Server to Cosmos DB? Or test without a real raven? You can't. You've got a maester problem.
// ✅ Depend on abstractions — both high and low level modules bow to the interface
public interface IMessageService
{
Task SendMessageAsync(string message, string recipient);
}
public interface IBattleLog
{
Task LogAsync(string target, DateTimeOffset timestamp);
}
// High-level module — knows nothing about SQL or ravens
public class BattleCommandService
{
private readonly IMessageService _messageService;
private readonly IBattleLog _battleLog;
public BattleCommandService(IMessageService messageService, IBattleLog battleLog)
{
_messageService = messageService;
_battleLog = battleLog;
}
public async Task CommandAttackAsync(string target)
{
await _messageService.SendMessageAsync($"Attack {target}", "AllCommandersNorth");
await _battleLog.LogAsync(target, DateTimeOffset.UtcNow);
}
}
// Low-level modules implement the abstractions
public class ServiceBusMessageService : IMessageService
{
private readonly ServiceBusSender _sender;
// ...
public async Task SendMessageAsync(string message, string recipient)
=> await _sender.SendMessageAsync(new ServiceBusMessage(message));
}
public class CosmosDbBattleLog : IBattleLog
{
public async Task LogAsync(string target, DateTimeOffset timestamp)
{ /* write to Cosmos */ }
}
Register in your DI container, and you're golden:
// In Program.cs or wherever you compose roots
builder.Services.AddScoped<IMessageService, ServiceBusMessageService>();
builder.Services.AddScoped<IBattleLog, CosmosDbBattleLog>();
builder.Services.AddScoped<BattleCommandService>();
Real talk: This is basically the backbone of Clean Architecture. Our application core in every enterprise project is a pure C# class library with zero Azure SDK references. The infrastructure layer does all the cloud-specific work behind interfaces. When we swapped from Logic Apps to Durable Functions in one project, the domain layer didn't even flinch. That felt like winning the Battle of the Bastards.
🐉 Now For the Design Patterns — The Great Houses Edition
🏹 Strategy Pattern — House Stark vs House Lannister
"Different problems. Different swords. Same kingdom."
The Strategy pattern lets you define a family of algorithms, encapsulate each one, and make them interchangeable.
public interface IBattleStrategy
{
string Execute(string enemy);
}
public class NorthernWarfare : IBattleStrategy
{
public string Execute(string enemy)
=> $"Guerrilla warfare in the snow against {enemy}. We know this land.";
}
public class LannisterWarfare : IBattleStrategy
{
public string Execute(string enemy)
=> $"Buy mercenaries, hire spies, bribe {enemy}'s bannermen. Cheaper than honor.";
}
public class TargaryenWarfare : IBattleStrategy
{
public string Execute(string enemy)
=> $"Send Drogon. Done. {enemy} is ash. Meeting adjourned.";
}
public class WesterosArmy
{
private IBattleStrategy _strategy;
public WesterosArmy(IBattleStrategy strategy) => _strategy = strategy;
public void SetStrategy(IBattleStrategy strategy) => _strategy = strategy;
public void GoToWar(string enemy) => Console.WriteLine(_strategy.Execute(enemy));
}
// Usage
var army = new WesterosArmy(new NorthernWarfare());
army.GoToWar("Bolton"); // Guerrilla in the snow
army.SetStrategy(new TargaryenWarfare());
army.GoToWar("Cersei"); // Dracarys. Season 8 was still a disappointment though.
🕵️ Observer Pattern — The Three-Eyed Raven
"Bran sees everything. Your event handlers should too."
Bran Stark is literally an Observer. He watches events unfold across time and space. When something happens, he knows. When multiple things happen at once, he knows all of them.
public interface IWesterosObserver
{
Task OnEventOccurredAsync(WesterosEvent westerosEvent);
}
public record WesterosEvent(string Type, string Description, DateTimeOffset OccurredAt);
public class ThreeEyedRaven // The event publisher
{
private readonly List<IWesterosObserver> _observers = new();
public void Subscribe(IWesterosObserver observer) => _observers.Add(observer);
public void Unsubscribe(IWesterosObserver observer) => _observers.Remove(observer);
public async Task PublishEventAsync(WesterosEvent evt)
{
Console.WriteLine($"[Raven] Observed: {evt.Type}");
foreach (var observer in _observers)
await observer.OnEventOccurredAsync(evt);
}
}
// Multiple observers react independently
public class NightWatchObserver : IWesterosObserver
{
public Task OnEventOccurredAsync(WesterosEvent evt)
{
if (evt.Type == "WhiteWalkerSighting")
Console.WriteLine("⚔️ NightWatch: Sending ravens. Preparing dragonglass.");
return Task.CompletedTask;
}
}
public class CerseiLannisterObserver : IWesterosObserver
{
public Task OnEventOccurredAsync(WesterosEvent evt)
{
if (evt.Type == "WhiteWalkerSighting")
Console.WriteLine("👑 Cersei: Not my problem. Let them fight each other first.");
return Task.CompletedTask;
}
}
Real talk: This is literally Azure Service Bus with multiple subscribers. We use this pattern all the time for domain events in clean architecture. Order placed → billing subscriber, inventory subscriber, notification subscriber all react independently. Bran would be proud.
🏗️ Builder Pattern — Building the Wall
"You don't build a 700-foot wall of ice in one function call."
public class TheWall
{
public int HeightInFeet { get; init; }
public bool HasMagicWards { get; init; }
public bool HasCastleBlack { get; init; }
public IReadOnlyList<string> DefenseFeatures { get; init; } = Array.Empty<string>();
public string Material { get; init; } = "Ice";
}
public class WallBuilder
{
private int _height = 700;
private bool _hasMagicWards = false;
private bool _hasCastleBlack = true;
private readonly List<string> _defenseFeatures = new();
private string _material = "Ice";
public WallBuilder WithHeight(int feet) { _height = feet; return this; }
public WallBuilder WithMagicWards() { _hasMagicWards = true; return this; }
public WallBuilder WithoutCastleBlack() { _hasCastleBlack = false; return this; }
public WallBuilder WithMaterial(string material) { _material = material; return this; }
public WallBuilder WithDefenseFeature(string feature)
{
_defenseFeatures.Add(feature);
return this;
}
public TheWall Build() => new TheWall
{
HeightInFeet = _height,
HasMagicWards = _hasMagicWards,
HasCastleBlack = _hasCastleBlack,
DefenseFeatures = _defenseFeatures.AsReadOnly(),
Material = _material
};
}
// Clean, readable construction
var originalWall = new WallBuilder()
.WithHeight(700)
.WithMagicWards()
.WithDefenseFeature("Warged dire wolves")
.WithDefenseFeature("Castle Black garrison")
.Build();
// The Night King's version (post-Season 7, we don't talk about it)
var brokenWall = new WallBuilder()
.WithHeight(0)
.WithMaterial("Rubble")
.WithoutCastleBlack()
.Build();
🎭 Decorator Pattern — Arya Stark's Faceless Man Training
"She's still Arya. Just... with extra capabilities bolted on."
The Decorator pattern lets you add behavior to objects dynamically without changing their class.
public interface IAssassin
{
string PerformMission(string target);
}
public class AryaStark : IAssassin
{
public string PerformMission(string target)
=> $"Arya kills {target} with her water dancer training.";
}
// Decorator adds face-changing ability
public class FacelessManDecorator : IAssassin
{
private readonly IAssassin _innerAssassin;
private string _currentFace = "Own Face";
public FacelessManDecorator(IAssassin assassin) => _innerAssassin = assassin;
public void WearFace(string face) => _currentFace = face;
public string PerformMission(string target)
{
var disguise = $"[Disguised as: {_currentFace}] ";
return disguise + _innerAssassin.PerformMission(target);
}
}
// Usage
var arya = new AryaStark();
var faceswapper = new FacelessManDecorator(arya);
faceswapper.WearFace("Old Woman");
Console.WriteLine(faceswapper.PerformMission("Walder Frey"));
// [Disguised as: Old Woman] Arya kills Walder Frey with her water dancer training.
faceswapper.WearFace("Sansa Stark");
Console.WriteLine(faceswapper.PerformMission("Night King"));
// [Disguised as: Sansa Stark] and then she goes full Arya. Iconic.
Real talk: We use this for Azure Function middleware pipelines — logging decorator, authentication decorator, retry decorator, all wrapping the core handler. Same function. Extra powers. No subclassing. Chef's kiss.
🦁 Singleton Pattern — The Iron Throne
"There is only one Iron Throne. And creating two is how civil wars start."
// Thread-safe Singleton using Lazy<T>
public sealed class IronThrone
{
private static readonly Lazy<IronThrone> _instance =
new Lazy<IronThrone>(() => new IronThrone());
public static IronThrone Instance => _instance.Value;
private string _currentMonarch = "Vacant";
private readonly object _lock = new();
private IronThrone()
{
Console.WriteLine("The Iron Throne has been forged. There can be only one.");
}
public string CurrentMonarch
{
get { lock (_lock) return _currentMonarch; }
}
public void CrownMonarch(string name)
{
lock (_lock)
{
Console.WriteLine($"{_currentMonarch} has been deposed. Long live {name}!");
_currentMonarch = name;
}
}
}
// Usage
IronThrone.Instance.CrownMonarch("Robert Baratheon");
IronThrone.Instance.CrownMonarch("Joffrey Baratheon"); // 🙄
IronThrone.Instance.CrownMonarch("Daenerys Targaryen"); // brief reign
IronThrone.Instance.CrownMonarch("Bran Stark"); // nobody asked
Console.WriteLine(IronThrone.Instance.CurrentMonarch); // Bran Stark. Still weird.
⚠️ Warning: Use Singleton sparingly. Like the Iron Throne, it looks powerful but causes wars. In ASP.NET Core, prefer the DI container's AddSingleton over manual implementations.
🗺️ Quick Reference: The Great Houses of Patterns
| Pattern | House | Use When |
|---|---|---|
| Strategy | Stark vs Lannister | Multiple interchangeable algorithms |
| Observer | Three-Eyed Raven | One event, many reactions |
| Builder | The Night's Watch | Complex object construction |
| Decorator | Arya (Faceless Men) | Add behavior without subclassing |
| Singleton | The Iron Throne | One instance, global access |
| Factory | Qyburn's Lab | Delegate object creation |
| Repository | The Citadel | Abstract data access |
| Mediator | The Small Council | Decouple components via a hub |
Parting Words
Alam mo na. (Now you know.)
SOLID principles aren't abstract philosophy. They're battle-tested survival rules for codebases that need to scale, be maintained by a team of 5+ developers, and survive the inevitable rewrite requests from product owners who "just want one small change."
Design patterns aren't silver bullets either — they're vocabulary. When your teammate says "let's use a Strategy here," you both immediately understand the structure, the trade-offs, and the intent. That shared language is worth gold dragons.
The best architecture I've ever built wasn't the most clever. It was the one where a new developer could look at the folder structure, trace a request through the layers, and understand every decision without asking me a single question.
Yan ang tunay na tagumpay. (That is true success.)
Now go forth. Refactor your GodService. Segregate your interfaces. And for the love of all that is holy, do not write your business logic in the controller.
Winter is coming. And so is the next sprint planning.
Did this post help? Disagree with any of the patterns? Drop a comment below — let's fight about it respectfully, unlike the War of the Five Kings.





