Over the course of this series, we’ve spent quite a bit of time discussing the benefits of PowerShell classes.
We’ve seen how they can:
- Improve maintainability
- Encapsulate complexity
- Enable reuse
- Simplify testing
- Support larger automation solutions
By now, you might think I’m a huge advocate of classes. I’m not. At least not in the way many people assume. The truth is that some of the worst PowerShell code I’ve ever encountered was object-oriented. Not because classes are bad. Because developers became more interested in using classes than solving the problem. And that brings us to the most important topic in this entire series:
Knowing when not to use classes.
Because good engineering isn’t about using advanced techniques. It’s about choosing the simplest solution that remains maintainable.
The PowerShell Identity Crisis
Many PowerShell developers eventually discover classes and experience a moment of enlightenment. Suddenly they can write:
class Server {}
instead of:
[PSCustomObject]@{}
Then:
class ServerManager {}
class MonitoringService {}
class BackupProvider {}
class NotificationEngine {}
Before long they’re building frameworks that look suspiciously like C# applications. At this point, something important often gets forgotten, PowerShell is still a scripting language. Its greatest strength has always been simplicity. Classes should enhance that simplicity, Not replace it.
The “Everything Must Be a Class” Anti-Pattern
Let’s start with the most common mistake. Suppose someone wants to collect a report. This is perfectly reasonable:
Get-Service | Select-Object Name, Status
Yet sometimes we see:
class Service {
[string]$Name
[string]$Status
}
class ServiceCollection {
[Service[]]$Items
}
class ServiceReportGenerator {
}
All to produce:
Name Status
----- ------
BITS Running
Spooler Running
The complexity has exceeded the problem. A useful question to ask:
Is the class helping me solve the problem, or am I solving the problem so I can use the class?
This distinction matters.
The Deep Inheritance Trap
Another common mistake is overuse of inheritance. In Part 3 we discussed inheritance. We allready mentioned that if used carefully, it can reduce duplication. when used enthusiastically, it creates chaos. Imagine an example like:
Resource
│
└── Computer
│
└── Server
│
└── WindowsServer
│
└── MonitoringEnabledServer
│
└── PatchManagedServer
By the time you reach the bottom of the hierarchy:
- Nobody understands where a property originates
- Debugging becomes difficult
- Refactoring becomes risky
The hierarchy has become more complicated than the system it’s modelling. Most PowerShell solutions benefit from 0-2 levels of inheritance, while very few benefit from5-10 levels of inheritance. If your inheritance diagram resembles a corporate organisation chart, you may have gone too far.
The Framework Obsession
Many engineers enjoy building frameworks. I know because I’ve been guilty of it myself. A project begins with a simple requirement:
“Check servers and generate a report.”
A couple of weeks later we have:
Framework
Execution Engine
Provider Model
Configuration Factory
Plugin Architecture
Resource Abstraction Layer
We have zero reports generated, but the architecture is beautiful, isn’t it? #Sarcasm.
This phenomenon is often called: Gold Plating. Adding sophistication nobody actually requested. While it’s, almost always, a waste of time, it’s also much harder to maintain.
Professional engineers optimise for outcomes, not architecture diagrams.
The God Class
Another anti-pattern appears when one class becomes responsible for everything.
class ServerManager {
[void] DiscoverServers() {}
[void] TestServers() {}
[void] GenerateReports() {}
[void] SendEmails() {}
[void] CreateTickets() {}
[void] BackupConfigurations() {}
[void] ExportData() {}
}
This class has become a miniature application. It violates the Single Responsibility Principle discussed in Part 4. Eventually these classes become impossible to maintain because every change affects everything else. Large classes typically indicate missing classes.
The Utility Class Dumpster
As projects grow, developers often encounter functionality that doesn’t have an obvious home. Instead of thinking carefully, they create:
class Utilities {}
# or
class Helper {}
This invariably becomes the place where everything goes. and six months later we see:
$helper.ConvertDate()
$helper.SendEmail()
$helper.FormatCurrency()
$helper.ConnectToSQL()
$helper.ExportCsv()
If a class could disappear and nobody would know where its methods belong, it probably lacks a clear responsibility.
Using Classes for Temporary Data
One area where many newcomers overuse classes is data storage.
class ReportRow {
[string]$ServerName
[string]$Status
[datetime]$Timestamp
}
Sometimes this is perfectly valid, but more often it isn’t. If the object exists solely to produce output, the following is frequently simpler.
[PSCustomObject]@{
ServerName = "SRV01"
Status = "Healthy"
Timestamp = Get-Date
}
Remember PSCustomObject is not inferior. It was designed for exactly this kind of scenario.
The Forgotten Superpower of PowerShell
PowerShell already has a powerful object model. Many solutions can remain beautifully simple.
Get-ADComputer -Filter * | Select-Object Name, OperatingSystem, LastLogonDate
No classes. No inheritance. No frameworks.
Just readable PowerShell. Never underestimate the value of straightforward code.
Signs You’re Overengineering
Over the years, I’ve noticed a few recurring warning signs that often indicate a design is becoming more complicated than it needs to be.
One of the first is when you spend more time designing and discussing the architecture than actually delivering automation. Good design is important, but eventually the solution needs to create value.
Another common sign is when new developers struggle to understand the codebase. If it takes significant effort to explain how everything fits together, the architecture may be working against you rather than helping you.
Complexity also tends to reveal itself when seemingly simple changes become difficult. If adding a small feature requires modifications across five different classes, the design may be introducing unnecessary coupling.
Having more base classes than concrete implementations is another warning signal. When a project contains more abstractions than actual working components, it’s often worth asking whether all that flexibility is genuinely needed.
And perhaps my favourite indicator: when the architecture diagrams are more impressive than the automation results themselves. It’s easy to become fascinated by elegant designs and sophisticated patterns, while losing sight of the original problem we’re trying to solve.
None of these signs automatically mean a design is bad. But when several of them start appearing together, it’s usually worth taking a step back and ask whether the solution has become more complex than the problem requires.
What Experienced PowerShell Engineers Do
Interestingly, experienced PowerShell engineers often move in the opposite direction from beginners. Beginners discover complexity and become excited. Experienced engineers discover simplicity and become ruthless. A typical evolution looks like:
Simple Scripts
↓
Object-Oriented Excitement
↓
Framework Obsession
↓
Architectural Regret
↓
Balanced Simplicity
Eventually many engineers arrive at a philosophy that sounds something like:
Use classes when they remove complexity. Avoid them when they add complexity.
Simple. Not always easy, but simple.
A Practical Decision Framework
When deciding whether to create a class, I find it helpful to start with a few simple questions. The answers won’t give you a definitive yes or no, but they can help determine whether a class is likely to add value or just introduce unnecessary complexity.
Does it represent a real-world concept?
Classes work best when they model something tangible within your automation domain.
Server
Certificate
User
VirtualMachine
These are all real entities with properties, state, and behaviour. If the object you’re modelling naturally exists in the problem domain, it’s often a strong candidate for a class.
Does it have meaningful behaviour?
A class should usually do more than just hold data. If an object has behaviour that belongs with the data it represents, a class can provide a natural home for that logic.
$server.Refresh()
$certificate.IsExpired()
$user.Disable()
In each case, the operation is directly related to the object itself. This is often a sign that a class is helping to create a clean and intuitive design.
Will it be reused across multiple scripts?
The more often a piece of functionality is reused, the more attractive classes become. Encapsulating logic in a class can improve consistency, reduce duplication, and make maintenance significantly easier over time.
For a one-off script, a class may be unnecessary. For functionality that appears throughout your automation estate, the benefits become much more compelling.
Is a PSCustomObject sufficient?
This is often the most important question of all. If a simple PSCustomObject meets your requirements, consider using one. Not every object needs constructors, methods, inheritance, or encapsulation.
Classes are a tool, not a goal. There is no award for using them when a simpler solution would do the job just as well.
The Final Rule
After all six parts of this series, here’s the rule I return to most often:
Solve the problem first. Choose the architecture second. Not the other way around.
When requirements genuinely demand classes, use them. When a pipeline solves the problem elegantly, embrace the pipeline. The best PowerShell solutions are rarely the most advanced. They’re the ones that are easiest to understand, easiest to maintain, and easiest to trust.
Series Summary
Throughout this series we’ve explored:
- Part 1 – Why PowerShell classes exist and when they become useful.
- Part 2 – Building practical classes around real-world automation needs.
- Part 3 – Inheritance, reuse, and the dangers of hierarchy.
- Part 4 – Designing reusable automation frameworks.
- Part 5 – Testing class-based solutions with Pester.
- Part 6 – Recognising when classes stop helping and start hurting.
Conclusion
PowerShell classes are neither a silver bullet nor an anti-pattern. They’re simply another tool. Sometimes that tool is exactly what you need. Sometimes it’s the wrong choice. The real skill isn’t knowing how classes work, it’s knowing when to reach for them. Because maintainable automation is not built through cleverness, it’s built through clarity. And clarity almost always wins.
Final Thoughts about the series
If there’s one takeaway from this series, I hope it’s this:
Classes are a tool for managing complexity, not a goal in themselves.
Used appropriately, they can help transform growing scripts into maintainable automation solutions. Used indiscriminately, they can introduce unnecessary complexity and slow development down.
I hope this series has given you both the confidence to use classes where they make sense and the judgement to avoid them where they don’t.
Thank you for following along, and I hope you’ve found the series insightful, practical, and valuable.

Leave a Reply