The goal of the state pattern is to encapsulate an object’s behaviors into a state for that object. Once an object abstracts its behaviors through state, it becomes easy to write simple and direct code that performs tasks and mutates its state when necessary. Let’s walk through this with an example and we’ll simplify.
For this case we have a very simple application that allows a user (via console) to operate and manage a trebuchet (french siege weapon https://en.wikipedia.org/wiki/Trebuchet). My application allows the user to take four actions:
- add a worker (someone needs to load the weapon, fire it, or make a repair)
- load ammo (needed to fire)
- fire it (boom…)
- make a repair (can be damaged by firing or misuse)
The user can perform tasks until the trebuchet is destroyed or they exit the application, and there are four permutations of trebuchet status that affect how the trebuchet behaves;

The basic entry point of our console program is as follows:
class Program { static void Main(string[] args) { var trebuchet = new Trebuchet(); Console.WriteLine("Welcome to the trebuchet test factory!"); while(trebuchet.DamageCount < 3) { Console.WriteLine(); Console.WriteLine("------------------------------------"); Console.WriteLine("What would you like to do with your trebuchet?"); Console.WriteLine("1 - Add Worker"); Console.WriteLine("2 - Load Ammo"); Console.WriteLine("3 - Fire"); Console.WriteLine("4 - Make Repair"); Console.WriteLine("------------------------------------"); Console.Write("Choice: "); int option = int.Parse(Console.ReadLine()); Console.WriteLine("------------------------------------"); Console.WriteLine(" - Executing Steps"); Console.WriteLine(); switch (option) { case (1): trebuchet.AddWorker(); break; case (2): trebuchet.LoadProjectile(); break; case (3): trebuchet.Fire(); break; case (4): trebuchet.MakeRepair(); break; default: Console.WriteLine(" - Your nonsense reply damaged the equipment... " + "shame on you. May god have mercy on your soul"); trebuchet.Ouch(); break; } Console.WriteLine(); Console.WriteLine(" - Execution Finished"); Console.WriteLine("------------------------------------"); } Console.WriteLine("The trebuchet is destroyed!"); } }
Under brief inspection, most of what’s there is just a lot of fluff surrounding the user making a choice on action to take and the trebuchet acting upon it. The trebuchet object holds the methods for performing actions and will be responsible for how to react to things like being asked to fire. I created a base class for my trebuchet to manage the fields holding data such as being loaded, damaged, or having staff. I also included the base operations around being damaged:
public class TrebuchetBase { protected bool _loaded; protected int _workerCount; public int DamageCount; internal void Ouch() { DamageCount++; Console.WriteLine($" - Trebuchet Has {GetDamageState()}"); } internal void Repair() { if (DamageCount > 0) { DamageCount--; } Console.WriteLine($" - Trebuchet Has {GetDamageState()}"); } private string GetDamageState() => DamageCount switch { 0 => "No Damage", 1 => "Light Damage", 2 => "Medium Damage", 3 => "Heavy Damage", _ => "Unknown Damage" }; }
Below is the implementation I created for my trebuchet without leveraging the state pattern. Note the amount of if conditions for the fire, repair, and load operations (in this basic example, it isn’t that cumbersome, but this could be wildly more complex in a real scenario).
public class Trebuchet : TrebuchetBase { public Trebuchet() { _loaded = false; _workerCount = 0; } public void AddWorker() { Console.WriteLine(" - Incrementing Worker Count!"); _workerCount++; } public void LoadProjectile() { if(_workerCount > 0 && !_loaded) // staffed and unloaded { _loaded = true; } else if (_workerCount == 0 && !_loaded) // unstaffed and unloaded { Console.WriteLine(" - Can't load projectile, no one to load it!"); } else if (_loaded) // staffed and loaded or unloaded { Console.WriteLine(" - Cannot Load Another Projectile!"); } } public void Fire() { if (_workerCount > 0 && !_loaded) // staffed and unloaded { Console.WriteLine(" - Dry firing trebuchet is bad for the equipment!"); Ouch(); } else if (_workerCount == 0 && !_loaded) // unstaffed and unloaded { Console.WriteLine(" - Can't fire, no ammo and no one to pull lever!"); } else if (_workerCount == 0 && _loaded) // unstaffed and loaded { Console.WriteLine(" - Cannot Fire Trebuchet - No One Working To Pull Lever!"); } else if (_loaded) // staffed and loaded or unloaded { Console.WriteLine(" - Firing Trebuchet!"); _loaded = false; if (new Random().Next(0, 10) <= 2) // 20% chance of catastrophic failure { Console.WriteLine(" - Catastrophic Failure at launch - Worker thrown with projectile!"); _workerCount--; Ouch(); } } } public void MakeRepair() { if (_workerCount > 0 && !_loaded) // staffed and unloaded { Console.WriteLine(" - Repairing Trebuchet Safely"); Repair(); } else if (_workerCount == 0 && !_loaded) // unstaffed and unloaded { Console.WriteLine(" - Can't repair trebuchet, no one available to do it"); } else if (_workerCount == 0 && _loaded) // unstaffed and loaded { Console.WriteLine(" - No Repair made - No Worker to Perform Repair!"); } else if (_workerCount > 0 && _loaded) // staffed and loaded or unloaded { Console.WriteLine(" - It is dangerous to repair a loaded Trebuchet - Loss of worker!"); _workerCount--; Repair(); } } }
Running the app, we can see that the behavior is as expected (below is the console output). I started by trying to fire the projectile, but failed due to no workers or ammo, then added a worker, loaded the projectile, and then finally fired it (and due to bad luck threw the worker).
Welcome to the trebuchet test factory!
------------------------------------
What would you like to do with your trebuchet?
1 - Add Worker
2 - Load Ammo
3 - Fire
4 - Make Repair
------------------------------------
Choice: 3
------------------------------------
- Executing Steps
- Can't fire, no ammo and no one to pull lever!
- Execution Finished
------------------------------------
------------------------------------
What would you like to do with your trebuchet?
1 - Add Worker
2 - Load Ammo
3 - Fire
4 - Make Repair
------------------------------------
Choice: 1
------------------------------------
- Executing Steps
- Incrementing Worker Count!
- Execution Finished
------------------------------------
------------------------------------
What would you like to do with your trebuchet?
1 - Add Worker
2 - Load Ammo
3 - Fire
4 - Make Repair
------------------------------------
Choice: 2
------------------------------------
- Executing Steps
- Loading Projectile!
- Execution Finished
------------------------------------
------------------------------------
What would you like to do with your trebuchet?
1 - Add Worker
2 - Load Ammo
3 - Fire
4 - Make Repair
------------------------------------
Choice: 3
------------------------------------
- Executing Steps
- Firing Trebuchet!
- Catastrophic Failure at launch - Worker thrown with projectile!
- Trebuchet Has Light Damage
- Execution Finished
------------------------------------
Switching to Having State
Now that’s out of the way, let’s take a look at the trebuchet with state and offloading the responsibility of trebuchet behaviors to the state object, which begins with an abstract class TrebuchetState.
public abstract class TrebuchetState { public abstract void Fire(TrebuchetWithState trebuchet); public abstract void LoadProjectile(TrebuchetWithState trebuchet); public abstract void AddWorker(TrebuchetWithState trebuchet); public abstract void MakeRepair(TrebuchetWithState trebuchet); }
Circling back to the four quadrant cube from the beginning of the post, I created four implementations of the trebuchet state:
public class UnloadedAndUnstaffedTrebuchetState : TrebuchetState { public override string ToString() { return "[Unloaded and Unstaffed]"; } public override void AddWorker(TrebuchetWithState trebuchet) { Console.WriteLine(" - Unloaded-And-Unstaffed: Adding Initial Worker!"); trebuchet.SetWorkerCount(trebuchet.GetWorkerCount() + 1); trebuchet.MutateState(new UnloadedAndStaffedTrebuchetState()); } public override void Fire(TrebuchetWithState trebuchet) { Console.WriteLine(" - Unloaded-And-Unstaffed: Can't fire, no ammo and no one to pull lever!"); } public override void LoadProjectile(TrebuchetWithState trebuchet) { Console.WriteLine(" - Unloaded-And-Unstaffed: Can't load projectile, no one to load it!"); } public override void MakeRepair(TrebuchetWithState trebuchet) { Console.WriteLine(" - Unloaded-And-Unstaffed: Can't repair trebuchet, no one available to do it"); } } public class UnloadedAndStaffedTrebuchetState : TrebuchetState { public override string ToString() { return "[Unloaded and Staffed]"; } public override void AddWorker(TrebuchetWithState trebuchet) { Console.WriteLine(" - Unloaded-And-Staffed: Incrementing Worker Count!"); trebuchet.SetWorkerCount(trebuchet.GetWorkerCount() + 1); } public override void Fire(TrebuchetWithState trebuchet) { Console.WriteLine(" - Unloaded-And-Staffed: Dry firing trebuchet is bad for the equipment!"); trebuchet.Ouch(); } public override void LoadProjectile(TrebuchetWithState trebuchet) { Console.WriteLine(" - Unloaded-And-Staffed: Loading Projectile!"); trebuchet.SetLoaded(true); trebuchet.MutateState(new LoadedAndStaffedTrebuchetState()); } public override void MakeRepair(TrebuchetWithState trebuchet) { Console.WriteLine(" - Unloaded-And-Staffed: Repairing Trebuchet Safely"); trebuchet.Repair(); } } public class LoadedAndUnstaffedTrebuchetState : TrebuchetState { public override string ToString() { return "[Loaded and Unstaffed]"; } public override void AddWorker(TrebuchetWithState trebuchet) { Console.WriteLine(" - Loaded-And-Unstaffed: Incrementing Worker Count!"); trebuchet.SetWorkerCount(trebuchet.GetWorkerCount() + 1); trebuchet.MutateState(new LoadedAndStaffedTrebuchetState()); } public override void Fire(TrebuchetWithState trebuchet) { Console.WriteLine(" - Loaded-And-Unstaffed: Cannot Fire Trebuchet - No One Working To Pull Lever!"); } public override void LoadProjectile(TrebuchetWithState trebuchet) { Console.WriteLine(" - Loaded-And-Unstaffed: Cannot Load Another Projectile!"); } public override void MakeRepair(TrebuchetWithState trebuchet) { Console.WriteLine(" - Loaded-And-Unstaffed: No Repair made - No Worker to Perform Repair!"); } } public class LoadedAndStaffedTrebuchetState : TrebuchetState { public override string ToString() { return "[Loaded and Staffed]"; } public override void AddWorker(TrebuchetWithState trebuchet) { Console.WriteLine(" - Loaded-And-Staffed: Incrementing Worker Count!"); trebuchet.SetWorkerCount(trebuchet.GetWorkerCount() + 1); } public override void Fire(TrebuchetWithState trebuchet) { Console.WriteLine(" - Loaded-And-Staffed: Firing Trebuchet!"); trebuchet.SetLoaded(false); if (new Random().Next(0, 10) <= 2) // 20% chance of catastrophic failure { Console.WriteLine(" - Loaded-and-Staffed: Catastrophic Failure at launch - Worker thrown with projectile!"); trebuchet.SetWorkerCount(trebuchet.GetWorkerCount() - 1); trebuchet.Ouch(); } if (trebuchet.GetWorkerCount() > 0) { // unloaded but still has workers trebuchet.MutateState(new UnloadedAndStaffedTrebuchetState()); } else { // unloaded and has no workers trebuchet.MutateState(new UnloadedAndUnstaffedTrebuchetState()); } } public override void LoadProjectile(TrebuchetWithState trebuchet) { Console.WriteLine(" - Loaded-And-Staffed: Cannot Load Another Projectile!"); } public override void MakeRepair(TrebuchetWithState trebuchet) { Console.WriteLine(" - Loaded-And-Staffed: It is dangerous to repair a loaded Trebuchet - Loss of worker!"); trebuchet.SetWorkerCount(trebuchet.GetWorkerCount() - 1); trebuchet.Repair(); if (trebuchet.GetWorkerCount() <= 0) { trebuchet.MutateState(new LoadedAndUnstaffedTrebuchetState()); } } }
Considering the code above the actions is direct, methods are built knowing the status of the object before taking action. If the action results in a change of state, then the state is simply mutated to a new one that matches the object. The only if conditions present in the trebuchet’s states are due to a small chance of catastrophic failure when firing. The trebuchet itself had its code updated to pass the actions to the state, and also needed a few methods added to help facilitate mutation of state in addition to setting private fields.
public class TrebuchetWithState : TrebuchetBase { private TrebuchetState _state; public TrebuchetWithState() { _state = new UnloadedAndUnstaffedTrebuchetState(); _loaded = false; _workerCount = 0; } internal string GetState() { return _state.ToString(); } public void AddWorker() { _state.AddWorker(this); } public void LoadProjectile() { _state.LoadProjectile(this); } public void Fire() { _state.Fire(this); } public void MakeRepair() { _state.MakeRepair(this); } internal void SetLoaded(bool loaded) { _loaded = loaded; Console.WriteLine($" - Trebuchet Loaded Status: {_loaded}"); } public int GetWorkerCount() { return _workerCount; } public void SetWorkerCount(int count) { _workerCount = count; Console.WriteLine($" - Current worker count: {_workerCount}"); } internal void MutateState(TrebuchetState state) { Console.WriteLine($" !! Trebuchet State Change - Changing State from {_state} to {state}"); _state = state; } }
Now, if we perform the same acts as before, we will get the same result (actually our trebuchet didn’t catastrophically fail in launch). Note that the print statements include the state prefix as well as a call out to when the state changes. In result the state changes when a worker is added, when ammo is loaded, and finally after firing.
Welcome to the trebuchet test factory!
------------------------------------
What would you like to do with your trebuchet?
1 - Add Worker
2 - Load Ammo
3 - Fire
4 - Make Repair
------------------------------------
Choice: 3
------------------------------------
- Executing Steps
- Unloaded-And-Unstaffed: Can't fire, no ammo and no one to pull lever!
- Execution Finished
------------------------------------
------------------------------------
What would you like to do with your trebuchet?
1 - Add Worker
2 - Load Ammo
3 - Fire
4 - Make Repair
------------------------------------
Choice: 1
------------------------------------
- Executing Steps
- Unloaded-And-Unstaffed: Adding Initial Worker!
- Current worker count: 1
!! Trebuchet State Change - Changing State from [Unloaded and Unstaffed] to [Unloaded and Staffed]
- Execution Finished
------------------------------------
------------------------------------
What would you like to do with your trebuchet?
1 - Add Worker
2 - Load Ammo
3 - Fire
4 - Make Repair
------------------------------------
Choice: 2
------------------------------------
- Executing Steps
- Unloaded-And-Staffed: Loading Projectile!
- Trebuchet Loaded Status: True
!! Trebuchet State Change - Changing State from [Unloaded and Staffed] to [Loaded and Staffed]
- Execution Finished
------------------------------------
------------------------------------
What would you like to do with your trebuchet?
1 - Add Worker
2 - Load Ammo
3 - Fire
4 - Make Repair
------------------------------------
Choice: 3
------------------------------------
- Executing Steps
- Loaded-And-Staffed: Firing Trebuchet!
- Trebuchet Loaded Status: False
!! Trebuchet State Change - Changing State from [Loaded and Staffed] to [Unloaded and Staffed]
- Execution Finished
------------------------------------
Drawback
So, what is the drawback to using the state pattern? As quantity of code and files explode for each permutation of possible state, a state must be created and implemented. In this simple scenario, I added a base state and four implementations. If there was greater complexity, this could explode into dozens or more.
Advantages
An object’s implementation of behaviors becomes exponentially more discrete as each is wholly based on a known set of conditions. Furthermore, adding additional states that an object could transition to would be reduced to adding the new state and mutations to previously existing states where necessary. This is in contrast to tackling conditional logic throughout the application.