Skip to content
All posts

The Client Said “Just a Small Change.” It Touched Eleven Files.

April 10, 2026·Read on Medium·

They said it would take an hour. It took three days. And the problem was not the change itself.

I have been doing this long enough to know what “small change” really means.

It means: “I do not know how the system works, but I do not think this is hard.”

The request came in on a Tuesday morning. The client wanted to add a new status to an order. One new status. They even told me which dropdown to update. In their head, this was a UI tweak. Thirty minutes, maybe.

By end of day I had touched eleven files, written two migration scripts, updated three event listeners, patched a PDF export, and broken a webhook integration I had forgotten existed.

Nobody was surprised except the client. That gap is the actual problem.

Why One Status Became Eleven Files

Let me walk through exactly what happened. This is not abstract. These are real layers that exist in most Laravel systems that grow without deliberate architecture.

The migration. New column on the orders table. Status stored as an enum. Fine.

The model. Cast updated. Status constants added. The model had a getStatusLabelAttribute accessor that needed a new entry. Two touches so far.

The policy. Access control checked specific statuses. The new one needed explicit permission handling or it would fall through as unauthorized. Three.

The form request. Validation rules used in: with a hardcoded list of valid statuses. Not pulling from a constant. Not pulling from the enum. Hardcoded. Four.

The resource. The API response transformer had a status_color field for the frontend badge. No color defined for the new status. Silent null in the response. Five.

The observer. An OrderObserver fired on every status change. It had a switch statement. The new status hit the default case and did nothing, but the client expected a specific action to happen. Six.

Two event listeners. One for internal audit logging. One for sending a notification. Both were listening to an OrderStatusChanged event that was never fired for the new status because nobody updated the observer. Seven and eight.

The PDF export. A generated invoice showed the order status as a label. The helper function that formatted it had a fallback of "Unknown". Client noticed this in testing. Nine.

The webhook payload. A third-party integration received status updates via webhook. Their system was validating against a fixed list of values on their end. New status caused their system to reject the payload silently. Ten.

The test. One feature test was asserting specific status transitions. It failed because the new status was not accounted for in the factory or the test data. Eleven.

One status. Eleven files. Zero surprises if you know the system. Total surprise if you do not.

This Is an Architecture Symptom, Not a Developer Mistake

I want to be clear about something. The eleven files were not a sign that the developer who built this did poor work. The system functioned correctly for everything it was asked to do before this change.

The problem is that status logic was never treated as a first-class concern. It lived in fragments across the codebase. Each fragment made sense locally. Nobody mapped the full picture.

This is how most systems grow. You add a status here, reference it there, handle a special case somewhere else. Over time the concept of “order status” stops living in one place and starts living everywhere. The system becomes load-bearing in ways that are invisible until you push.

There is a name for this. In software design it is called shotgun surgery: a single conceptual change forces modifications in many unrelated places. Martin Fowler identified this as a code smell in Refactoring (1999). It is the opposite of cohesion. Logic that belongs together is scattered apart.

The fix is not to avoid changes. The fix is to design for change.

“If It Touched Eleven Files, It Was Badly Built”

Some people will read this and say the real problem is the architecture. That if the system were properly designed, a new status would touch one file and nothing else.

They are not wrong. And they are also describing a system that does not exist in production.

Clean architecture is a goal, not a state you achieve and maintain forever. Systems get built under deadlines. Requirements change after launch. The team that built version one is not always the team maintaining version three. Technical debt is not a moral failure. It is the compound interest on every reasonable shortcut taken under pressure.

The eleven-file problem does not happen because developers are careless. It happens because software grows. A status field that started as three values becomes seven. An observer that handled two cases now handles nine. Nobody redesigns the whole thing because the business cannot stop while you refactor.

The real question is not “was this built perfectly?” It is “do you understand your own system well enough to know what a change will cost before you commit to it?”

That is the skill. Not pristine code. Accurate prediction.

What I Would Design Differently

If I were starting this system today, status handling would be a proper domain object.

Use a PHP-backed enum.

Since PHP 8.1, backed enums are the right tool for a fixed set of values. Instead of string constants scattered across files or hardcoded in: rules, you define the status once:

enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';

public function label(): string
{
return match($this) {
self::Pending => 'Pending',
self::Processing => 'Processing',
self::Shipped => 'Shipped',
self::Delivered => 'Delivered',
self::Cancelled => 'Cancelled',
};
}
public function color(): string
{
return match($this) {
self::Pending => 'yellow',
self::Processing => 'blue',
self::Shipped => 'indigo',
self::Delivered => 'green',
self::Cancelled => 'red',
};
}
}

The label and color live with the status. Not in an accessor. Not in a resource transformer. Not in a frontend helper. You add a new case, you add its label and color in the same file. Done.

Cast it on the model.

protected $casts = [
'status' => OrderStatus::class,
];

Now $order->status gives you an enum instance, not a raw string. The rest of the system works with typed values.

Validate using the enum.

'status' => ['required', Rule::enum(OrderStatus::class)],

No more hardcoded in: strings. The validation rule updates automatically when you add a new case.

Use a status transition map.

If your business logic controls which status can move to which, define it explicitly:

public function canTransitionTo(OrderStatus $new): bool
{
$allowed = match($this) {
self::Pending => [self::Processing, self::Cancelled],
self::Processing => [self::Shipped, self::Cancelled],
self::Shipped => [self::Delivered],
default => [],
};

return in_array($new, $allowed, true);
}

Now invalid transitions are caught at the domain level, not discovered in production.

The Conversation You Need to Have Before You Say Yes

Here is the part nobody talks about. The architecture is one problem. The expectation gap is another.

When a client says “small change,” they are giving you a product description, not a technical estimate. Those are different things. Your job is to bridge that gap before work starts, not after.

This is the process I run now before committing to any scope estimate.

1. Name every model the change touches. Not UI elements. Models. If the change involves orders, ask: does it affect order history, order items, order status, related users, related payments? Each model is a surface area.

2. Trace the side effects. For each model, ask: what fires when this changes? Observers, events, listeners, jobs, webhooks, notifications. These are the invisible dependencies that bite you.

3. Check the output layer. Does this appear in any export, report, PDF, API response or third-party integration? Each one is a separate touch.

4. Check the test coverage. Are there tests that assert the current behavior? They will need updating. No tests means you have no safety net during the change.

5. Give the estimate with a surface area summary, not just a number. Do not say “three days.” Say “this change touches the order model, two event listeners, the PDF export and the webhook integration. Three days.” The client now understands why. You now have written scope.

This does not prevent scope creep. It makes scope visible. There is a difference.

The Real Lesson

The client was not wrong to call it a small change. From where they sit, it looked small. One dropdown. One new value.

You are the one who can see what they cannot. That is not a burden. That is your value.

The mistake is accepting the description without doing the analysis. The mistake is quoting a number before tracing the surface area. The mistake is letting “it should be simple” become the estimate.

“Just a small change” is not a scope. It is an opening bid.

Treat it like one.

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium
The Client Said “Just a Small Change.” It Touched Eleven Files. — Hafiq Iqmal — Hafiq Iqmal