دفتر سليمان

Article

Every Exception Becomes Architecture

Exceptions often begin as harmless accommodations: one special case, one temporary branch, one customer-specific rule. Over time, they accumulate into hidden architecture. This article explores how to distinguish necessary exceptions from accidental complexity, and how to keep special cases from quietly becoming the system.

Most complicated systems did not begin complicated.
They became complicated one reasonable decision at a time.
A customer needed a slightly different rule.
A legacy integration could not support the new format.
A deadline required a temporary workaround.
One department needed an extra approval.
A particular report needed a special calculation.
A mobile client could not yet support the new API.
So we added an exception.
The exception made sense.
Then another one did too.
Eventually the system no longer had one clear path.
It had a main path surrounded by a growing constellation of special cases.
At that point, the exceptions were no longer sitting at the edges of the architecture.
They were the architecture

Exceptions Are Not Automatically Bad

Software engineers sometimes talk about exceptions as though every deviation from a general rule is a design failure.
Reality is more complicated.
Real systems serve real environments.
Different countries have different regulations.
Different hospitals may have different workflows.
Old integrations may need compatibility.
Users may operate under constraints that the original design did not anticipate.
A special case can be exactly the right response.
The problem is not that exceptions exist.
The problem begins when the system stops knowing why they exist, who they serve, and whether they still need to exist.
That is when accommodation becomes sediment.
Layer after layer remains in place until nobody is sure which parts are still carrying weight.

The First Exception Feels Cheap

Imagine a system with one rule:

All reports use the same rendering path.

Then one old report cannot support it.
The easiest solution is obvious.
Add a condition.
If report equals X, use the old path.
Everything else continues normally.
That may be perfectly sensible.
The cost appears tiny.
One branch.
One comment.
One deployment.
Now a second report has a similar problem.
We add another branch.
Then a facility has a special requirement.
Another branch.
Then one language behaves differently.
Another branch.
Nothing dramatic happens on the day each exception is added.
But the mental model changes.
The system is no longer:

Reports use this rendering path.

It becomes:

Reports usually use this rendering path, except these cases, unless this configuration applies, except for this facility, and unless the report is older than this migration.

The implementation may still run.
The architecture has become harder to state.
That is a warning sign.

Complexity Often Enters Through Reasonable Doors

This is why technical debt can be difficult to recognize.
Bad decisions are easy to condemn in hindsight.
Reasonable decisions are more dangerous because they survive scrutiny.
A developer says:

We need this condition because the legacy service cannot change yet.

True.
A product manager says:

This customer needs the field now.

True.
Operations says:

We cannot migrate this environment before release.

Also true.
Each decision may be correct locally.
The accumulation can still be harmful globally.
Architecture is partly the art of noticing when individually reasonable exceptions begin to damage the coherence of the whole.

A Special Case Creates a New Rule

This is the part we often underestimate.
When we add an exception, we are not merely modifying one case.
We are adding a new rule to the system.
Suppose the original logic is:

All users authenticate through the shared identity service.

Then an integration is allowed to bypass it using a separate credential path.
Now the architecture contains two authentication rules.
That affects more than login.
It affects:
security review,
logging,
support,
testing,
deployment,
documentation,
incident response,
and future integration decisions.
The exception has acquired a shadow.
Every special path creates surrounding obligations.
This is why the implementation size of an exception rarely represents its true cost.

Count Paths, Not Lines

A small amount of code can create enormous behavioral complexity.
Consider:

if A
else if B
else

Only a few lines.
But combine that with another condition elsewhere:

if X
else

Now the system may have several combinations of behavior.
Add a facility rule.
Add a language rule.
Add a feature flag.
Add a permission state.
The number of possible paths grows much faster than the source code appears to grow.
Testing becomes harder because correctness depends on combinations.
Documentation becomes harder because descriptions need qualifications.
Debugging becomes harder because two users can perform apparently identical actions and receive different behavior.
This is one reason architecture should be evaluated in terms of behavioral paths, not only implementation size.

Exceptions Hide in Configuration Too

Not every special case appears as an if statement.
Some live in configuration.
One tenant has a flag.
Another has a different timeout.
A third has a custom field mapping.
One report has its own template.
A particular environment uses a different endpoint.
Configuration can be the correct place for variability.
But configurable does not mean simple.
A system with fifty independently configurable behaviors may be more difficult to reason about than one with visible branches in code.
Configuration moves complexity.
It does not erase it.
The important question is whether the variations represent a coherent model or merely a collection of remembered exceptions.

Temporary Is an Architectural Category

Some of the most durable architecture begins with a sentence:

This is temporary.

Temporary workarounds are often necessary.
A service is unavailable.
A migration cannot happen yet.
A release must proceed.
The system needs a bridge.
There is nothing wrong with that.
The mistake is treating temporary as meaning:

We do not need to design this carefully.

A temporary solution still becomes part of the architecture for however long it exists.
It should therefore have boundaries.
Why does it exist?
What does it replace?
Who owns it?
What condition allows us to remove it?
What happens if it remains for a year?
Those questions turn a workaround into a controlled exception rather than an accidental permanent feature.

Give Exceptions an Expiration Condition

Dates can help, but conditions are often better.
Instead of:

Remove this in three months.

write:

Remove this after all clients move to API v3.

Or:

Remove this path when the legacy report set reaches zero active consumers.

Or:

Remove this override when the shared authorization service supports delegated access.

The advantage is that the exception is tied to the constraint that created it.
If the constraint disappears earlier, the exception can disappear earlier.
If the constraint remains longer, the reason is still visible.
An expiration condition prevents temporary architecture from becoming invisible architecture.

Name the Exception

Unnamed exceptions are difficult to manage.
Imagine encountering code like:

if (facilityId == 4012)
{
    // special handling
}

Why 4012?
Who requested this?
What is special about it?
Can the branch be removed?
Does another facility need the same behavior?
A named concept is much safer.
Perhaps the real rule is:

Facilities using legacy admission numbering require compatibility formatting.

Now the exception has meaning.
The facility ID becomes data.
The behavior becomes a defined capability or compatibility mode.
The code begins to describe the reason rather than the historical accident.
Names turn isolated hacks into visible concepts.
Sometimes that reveals that the “exception” should actually become part of the domain model.

Repeated Exceptions Are Trying to Tell You Something

The second or third exception of the same kind deserves attention.
Suppose one customer needs custom approval rules.
Then another does.
Then a third.
Perhaps these are not exceptions anymore.
Perhaps approval policy is genuinely variable.
The architecture should represent that variability explicitly.
This is an important transition.
At first:

Special case.

Later:

Pattern.

Eventually:

Model.

Good architecture notices when the system has crossed that boundary.
If we keep representing a recurring concept through scattered exceptions, the implementation fights the domain.

Promote Patterns Carefully

There is a risk in the opposite direction.
One exception appears, and we immediately build a generalized framework.
A customer asks for one custom field.
We invent a universal dynamic-schema engine.
A report needs one unusual filter.
We create a report-expression language.
A workflow needs one branch.
We build a general workflow platform.
This can be as damaging as endless hardcoding.
The first exception tells us only that one exception exists.
It does not prove a general pattern.
The second may suggest something.
The third often gives us enough evidence to examine whether there is a stable underlying concept.
Generalization should follow observed repetition, not speculative enthusiasm.

Make the Main Path Obvious

Healthy systems usually have a clear default path.
If I ask:

How does publishing work?

there should be a reasonably concise answer.
If the explanation begins immediately with exceptions, the architecture may already be too fragmented.
The default path matters because it becomes the mental model people use to understand the system.
Exceptions should branch from that model without obscuring it.
This suggests a useful design goal:
Make normal behavior easy to understand, and make exceptional behavior easy to identify.
Do not intertwine them so deeply that neither can be described independently.

Isolate Exceptions at Boundaries

One way to protect the main path is to isolate special behavior near an appropriate boundary.
Suppose one external provider returns dates in an unusual format.
The worst place to handle that exception may be everywhere the date is used.
A better place is often the integration boundary.
Normalize the provider-specific representation once.
Inside the system, use the normal model.
This principle appears repeatedly in architecture.
Compatibility logic belongs near compatibility boundaries.
Presentation exceptions belong near presentation.
Provider-specific behavior belongs near provider adapters.
Legacy translation belongs near legacy interfaces.
The closer an exception is to the boundary that causes it, the less it contaminates the rest of the system.

Do Not Let Exceptions Redefine Ownership

Some special cases create much larger problems because they blur system boundaries.
Imagine Platform A owns project metadata.
Platform B needs one field urgently.
Instead of referencing Platform A, the team duplicates the field inside Platform B.
Fast.
Then another field is added.
Then status.
Then category.
Soon Platform B has become a partial project database.
The original exception quietly redefined ownership.
That is much more dangerous than a local conditional branch.
It creates two versions of truth.
Now synchronization itself becomes architecture.
Some exceptions should therefore be rejected not because they are technically difficult, but because they violate a boundary that protects coherence.

Compatibility Deserves Respect

Legacy behavior is a common source of exceptions.
It is fashionable to dismiss old interfaces as technical debt.
Sometimes they are.
Sometimes they are contracts.
A strange parameter may exist because another application still sends it.
A nullable field may preserve compatibility with an older database procedure.
An odd report layout may satisfy an external regulatory requirement.
Removing such behavior simply because it looks inelegant can break real users.
The correct question is not:

Is this old?

It is:

Does anything still rely on it?

Then:

Can that dependency be migrated?

Compatibility work becomes healthier when exceptions are attached to known consumers.
When the last consumer disappears, the compatibility branch can disappear too.

Tests Should Reveal Exceptions

If an exception matters enough to exist, it usually matters enough to test.
A special path with no test is an oral tradition expressed in code.
Eventually someone will change nearby behavior and forget that the exception exists.
A test does more than protect execution.
It documents that this behavior is intentional.
A well-named test might say:

Legacy facilities preserve external admission number formatting.

That sentence tells the next developer something important.
This is another way of externalizing knowledge.
The system does not merely contain the exception.
It contains evidence that the exception is expected.

Exceptions Need Observability

Special paths can fail differently from normal paths.
If they are important, we should be able to see when they are used.
Suppose a compatibility branch is meant to disappear gradually.
How many requests still use it?
Which clients?
How often?
Without observability, the branch may remain forever because nobody knows whether it is safe to remove.
A simple metric can transform the problem.

Legacy path usage: 0 requests for 60 days.

Now removal becomes a much easier decision.
The exception moves from folklore to measurable state.

Removal Is Part of the Design

We often design how an exception enters the system.
We rarely design how it leaves.
That is a mistake.
When adding a special path, ask:
What evidence will tell us it is no longer needed?
What must be migrated first?
What test will be removed?
What configuration can disappear?
What documentation must change?
What data cleanup is required?
This is architectural hygiene.
An exception with no exit strategy is quietly applying for permanent residence.

Exception Budgets Can Be Useful

There is no universal number of acceptable exceptions.
But teams benefit from noticing accumulation.
One way is to periodically ask:
How many compatibility paths exist?
How many tenant-specific rules?
How many feature flags are permanent?
How many temporary migrations remain?
How many configuration overrides have no owner?
The point is not to create a bureaucratic score.
It is to expose where the architecture is fragmenting.
Every exception consumes a small portion of the system’s cognitive budget.
Enough of them make ordinary changes surprisingly expensive.

Complexity Appears in Support First

Developers often feel the pain of exceptions during maintenance.
Support teams may feel it even earlier.
A user reports unexpected behavior.
The support person checks the normal rule.
It should work.
Then someone remembers:

That account is on the old workflow.

Or:

That facility has a special configuration.

Or:

That module behaves differently because of the migration.

Now diagnosis depends on hidden history.
When support conversations repeatedly include “except for,” the architecture is sending a signal.
The exception model may need to become more explicit.

Product Consistency Is Architectural Too

Exceptions are not only a backend concern.
They affect user experience.
Imagine the same Save action behaves differently across three modules.
One autosaves.
One requires confirmation.
One publishes immediately.
All three behaviors may have historical reasons.
To the user, the product feels inconsistent.
Consistency reduces learning cost.
Exceptions increase it.
This does not mean every screen must behave identically.
Different contexts can justify different behavior.
But differences should follow meaning, not implementation history.
If users cannot infer behavior from context, the product is exposing its internal exceptions.
That is rarely desirable.

Personal Systems Accumulate Exceptions Too

This pattern is not limited to software.
Personal workflows do the same thing.
You create one rule for organizing files.
Then one project gets a special folder.
Another is stored somewhere else because of a tool.
A third uses a different naming convention.
Soon finding anything requires memory.
Or consider task management.
One category lives in a task app.
Another in email.
One project uses a spreadsheet.
Urgent items live in chat.
Nothing is individually unreasonable.
The total system becomes expensive to maintain.
Personal systems also benefit from asking:
Is this exception still necessary?
Could this special case return to the default path?
Sometimes simplification means not inventing a better system.
It means deleting old exceptions.

Every Exception Should Answer Four Questions

When a special case is truly needed, I want it to answer four things clearly:
Why does it exist?
Where is it isolated?
How do we know when it is used?
What allows us to remove it?

If we cannot answer those questions, the exception is likely to become hidden architecture.
The implementation may still be correct.
The future cost remains unknown.

The Goal Is Not Purity

Perfectly uniform systems are often unrealistic.
The real world contains irregularity.
Good architecture does not try to erase all of it.
It decides where irregularity is allowed to enter.
It gives unusual cases names.
It prevents them from leaking everywhere.
It observes them.
It removes them when their purpose disappears.
This is not architectural purity.
It is stewardship.

From Handoffs to Exceptions

Yesterday’s article argued that work should carry context when responsibility changes hands.
Today extends the same idea through time.
An exception is also a kind of handoff.
The developer creating it is handing a piece of unusual behavior to whoever encounters the system later.
That future person deserves context.
Why is this here?
Is it intentional?
What depends on it?
Can I remove it?
Without those answers, the exception becomes a riddle embedded in production.
With them, it becomes a controlled part of the system.
That difference matters.

Architecture Is What Remains

We often imagine architecture as the diagrams drawn before implementation.
In practice, architecture is also what remains after hundreds of small decisions.
The normal paths.
The boundaries.
The data ownership.
The defaults.
The exceptions.
Especially the exceptions.
Because every special case that survives long enough changes how the system must be understood.
So when a reasonable request arrives and the easiest solution is:

Just add an exception.

Maybe that is exactly what you should do.
But recognize the transaction.
You are not adding one small line.
You are adding one more rule to the future mental model of the system.
Give it a reason.
Give it a boundary.
Give it evidence.
Give it an exit.
Because every exception that stays long enough eventually becomes architecture.
And architecture should be intentional.

Filed under

  • Building What Matters
  • Systems Thinking
  • Software Architecture
  • Product Design
  • Decision Making
  • Technical Debt
  • Maintainability