A Case Against Static Orchestration
Just because you can call everything statically doesn't mean you should.
I’m not saying all static functions are bad, in fact…
Static methods absolutely have their place.
They're simple to call:
Str::slug($title);No object creation.
No dependency injection.
No setup.
Just call the method and move on with your day.
They can make utility-style code feel clean and expressive.
They’re often great for:
String manipulation.
Mathematical calculations.
Formatting values.
Data transformations.
Factory-style object creation.
Stateless helper functions.
In the right context, they’re perfectly reasonable.
However…
The problem is when a static method starts behaving like a project manager.
Instead of doing one thing, it starts coordinating five other things.
Then those things coordinate five more things.
Suddenly your innocent little static method has become the entire plot of Tango and Cash,
There’s a mouse-obsessed crime lord appears for reasons nobody can adequately explain.
There are monster trucks, explosions, secret plans and there's a completely unnecessary shower scene..
Basically it’s FUBAR. But why? Well… take this.
ReportGenerator::generate($report);
Looks harmless.
Professional.
Clean.
Respectable.
Then you open the method and discover it’s secretly doing this:
class ReportGenerator
{
public static function generate(Report $report): void
{
static::validate($report);
static::transform($report);
static::persist($report);
static::notify($report);
}
private static function validate(Report $report): void
{
static::validateReportExists($report);
static::validatePermissions($report);
static::validateBusinessRules($report);
}
private static function transform(Report $report): void
{
static::normaliseData($report);
static::calculateTotals($report);
static::generateSummary($report);
}
private static function persist(Report $report): void
{
static::saveReport($report);
static::saveAuditLog($report);
static::saveMetrics($report);
}
private static function notify(Report $report): void
{
static::notifyOwner($report);
static::notifyManagers($report);
static::dispatchWebhook($report);
}
private static function validateReportExists(Report $report): void {}
private static function validatePermissions(Report $report): void {}
private static function validateBusinessRules(Report $report): void {}
private static function normaliseData(Report $report): void {}
private static function calculateTotals(Report $report): void {}
private static function generateSummary(Report $report): void {}
private static function saveReport(Report $report): void {}
private static function saveAuditLog(Report $report): void {}
private static function saveMetrics(Report $report): void {}
private static function notifyOwner(Report $report): void {}
private static function notifyManagers(Report $report): void {}
private static function dispatchWebhook(Report $report): void {}
}
One static method calling four other static methods.
Then those static methods call more static methods.
Then those static methods call even more static methods.
Before long you’ve got a static Russian nesting doll.
Why This Becomes a Problem
The first issue is hidden dependencies.
When I see:
ReportGenerator::generate($report);
I have no idea what it actually needs.
Does it hit the database?
Send emails?
Dispatch jobs?
Talk to an API?
Summon an ancient demon?
Everything is hidden behind static calls.
The second issue is testing.
Testing static-heavy code is possible, but it’s usually more awkward than it needs to be.
You can’t easily replace collaborators.
You can’t inject test doubles.
You can’t swap implementations.
You often end up testing a huge chain of behaviour all at once because everything is tightly coupled together.
The Alternative
Instead of creating giant static orchestration methods, I prefer a simple static constructor (if you absolutely do not want to new up an object).
Something like:
ReportGenerator::instance($report)
->validate()
->transform()
->persist()
->notify();The static method only has one responsibility:
class ReportGenerator
{
public static function instance(Report $report): self
{
return new self($report);
}
}
That’s it. Now the actual work happens through instance methods.
class ReportGenerator
{
public __construct(private Report $report) {
//
}
public static function instance(Report $report): self
{
return new self($report);
}
public function validate(): self
{
// Validation business logic here
// Check permissions
// Verify report exists
// Ensure business rules are satisfied
return $this;
}
public function transform(): self
{
// Transformation business logic here
// Normalise data
// Calculate totals
// Generate summary information
return $this;
}
public function persist(): self
{
// Persistence business logic here
// Save report
// Write audit records
// Store metrics
return $this;
}
public function notify(): self
{
// Notification business logic here
// Notify owner
// Notify managers
// Dispatch webhooks
return $this;
}
}Why I Prefer This
There’s a few key advantages.
Dependencies have somewhere to go
If you need to inject a dependency the static version usually ends up doing this:
public static function generate(Report $report): void
{
static::validate($report);
$repository = new ReportRepository();
$repository->save($report);
static::notify($report);
}With an instance, you can move towards:
public function __construct(
private ReportRepository $repository,
private Notifier $notifier,
) {
//
}
public static function instance(ReportRepository $repository, Notifier $notifier): self
{
return new self(
new $repository,
new $notifier,
);
}Now the class admits what it needs.
It is easier to test
The problem is what happens when those static methods depend on other things.
If your static method quietly creates its own dependencies:
private static function persist(Report $report): void
{
$repository = new ReportRepository();
$repository->save($report);
}You now have a problem.
How do you replace
ReportRepositoryin a test?How do you swap it for a fake?
How do you stop it from touching the real database?
How do you make it throw a controlled exception so you can test the failure path?
You can test the method, sure.
But controlling what the method talks to becomes awkward.
Not saying, all of them above isn’t possible. It is. But it’s needlessly awkward.
With an instance, the dependency can be supplied from the outside:
$generator = new ReportGenerator(
repository: $fakeRepository,
notifier: $fakeNotifier
);Now your test is in control.
You can pass in a fake repository.
You can pass in a fake notifier.
You can make one dependency fail without dragging the entire application into the test.
That is the real benefit.
State is handled more naturally
In the static version, every method needs the report passing through it:
public static function generate(Report $report): void
{
static::validate($report);
static::transform($report);
static::persist($report);
static::notify($report);
}Then:
private static function validate(Report $report): void
{
//
}
private static function transform(Report $report): void
{
//
}
private static function persist(Report $report): void
{
//
}
private static function notify(Report $report): void
{
//
}That doesn’t seem like a huge problem initially.
It’s only one parameter.
Then six months later the requirements change.
Now every method needs:
private static function transform(
Report $report,
User $user,
Settings $settings,
FeatureFlags $flags
): void {
//
}And then every other method needs the same thing:
private static function persist(
Report $report,
User $user,
Settings $settings,
FeatureFlags $flags
): void {
//
}You’re effectively passing the same collection of objects through half the class because static methods have nowhere to store state.
With an instance, you can store the report once:
public __construct(private Report $report) {
//
}
It is easier to extend later
The static version usually gives you one big entry point:
ReportGenerator::generate($report);That is fine while every report follows the exact same process.
But what happens when one report needs an extra step?
Before long, generate() starts turning into this:
public static function generate(Report $report, bool $notify = true, bool $archive = false): void
{
static::validate($report);
static::transform($report);
static::persist($report);
if ($notify) {
static::notify($report);
}
if ($archive) {
static::archive($report);
}
}Then six months later:
public static function generate(
Report $report,
bool $notify = true,
bool $archive = false,
bool $requiresApproval = false,
bool $previewOnly = false,
): void {
static::validate($report);
if ($requiresApproval) {
static::requestApproval($report);
}
static::transform($report);
if (! $previewOnly) {
static::persist($report);
}
if ($notify) {
static::notify($report);
}
if ($archive) {
static::archive($report);
}
}With the instance version, the flow can be more intentional:
ReportGenerator::instance($report)
->validate()
->transform()
->persist()
->notify();Need to add a new step?
ReportGenerator::instance($report)
->validate()
->transform()
->persist()
->doSomethingElseHere()
->notify();Need to skip notifications in one place?
ReportGenerator::instance($report)
->validate()
->transform()
->persist();Need a different flow for a different report type?
ReportGenerator::instance($report)
->validate()
->transform()
->requestApproval()
->persist()
->notify();I’ll stop now. The important thing is that the workflow is controlled by the caller, not buried inside one increasingly stressed generate() method.
You can compose the steps you actually need instead of adding another flag, another condition, another optional parameter etc.
The Trade-Off
The instance version is not automatically better.
If the logic is tiny, pure, and stateless, static is fine.
But once the method starts coordinating validation, persistence, notifications, jobs, APIs, logs, metrics, and whatever else wandered in wearing sunglasses, I’d rather move that orchestration into an object.
The static method should open the door.
It should not run the building.
P.S. Now go and watch Tango & Cash. It is absolutely ridiculous, completely unnecessary, and somehow still brilliant.





I know Sylvester Stallone without having watched any of his films.
Can this count as a superpower? haha, anyway.