DEV Community

Nirmal Jingar
Nirmal Jingar

Posted on

Before You Refactor Legacy Code, Map the Decisions Hidden Inside It

Before you refactor legacy code, ask one question:

What decision am I about to change?

Not what class am I cleaning up.
Not what service am I extracting.
Not what function am I rewriting.
Not what dependency am I removing.

What decision am I changing?

That question would have saved me from one of the most expensive mistakes I have seen in software modernization.

A change looked safe. It passed code review. It passed tests. It passed the approval gates.

Within 48 hours, it cost millions.

Nothing crashed. No alarms fired. The system kept running.

It just started making worse decisions.

That is the kind of failure that is hard to catch with normal engineering checks. The code works. The deployment succeeds. The metrics may even look normal at first.

But the business outcome changes.

The lesson was simple:

We did not break the system because we changed code. We broke it because we changed a decision we did not fully understand.

Legacy code is not just messy implementation

When engineers inherit legacy code, it is natural to see the mess first.

Long methods.
Nested conditionals.
Duplicated logic.
Old comments.
Strange defaults.
Special cases.
Branches nobody wants to touch.

The instinct is to clean it up.

That instinct is often right. Legacy code can slow teams down. It can make releases risky. It can hide bugs. It can increase operational cost. It can make onboarding painful.

But there is a trap.

Some of that mess is not just bad implementation.

Some of it is hidden business logic.

Some of it is decision debt.

Technical debt is code that is hard to change.

Decision debt is behavior that no one can explain anymore.

That distinction matters.

If you treat decision debt like simple technical debt, you can accidentally delete business knowledge.

What hidden business logic looks like

Hidden business logic rarely announces itself clearly.

It usually looks like ordinary code.

A condition.
A fallback.
A default value.
A mapping.
An exception.
A temporary workaround that became permanent.
A comment that made sense five years ago.

Here are common places where I look for it:

  • Eligibility rules
  • Routing logic
  • Pricing rules
  • Fallback behavior
  • Customer exceptions
  • Operational constraints
  • Vendor workarounds
  • Data assumptions
  • Old compensations for external system bugs
  • Hardcoded thresholds
  • Manual overrides
  • Feature flags with unclear ownership
  • Special handling for rare states
  • Logic that protects downstream systems

This is why refactoring legacy code is not only a software architecture problem.

It is also a recovery problem.

You are trying to recover the intent behind behavior before you change the behavior.

Load-bearing decisions

A useful way to think about this is the idea of a load-bearing decision.

In a house, many walls can be moved. You can redesign the layout, open up space, and make the structure feel new.

But some walls are load-bearing.

They hold up parts of the structure you cannot see.

Legacy systems have the same pattern.

Some decisions are safe to change. They are cosmetic, obsolete, duplicated, or purely technical.

Other decisions are load-bearing.

They may protect:

  • Revenue
  • Customer experience
  • Operational workflows
  • Edge cases
  • Compliance constraints
  • Performance under specific conditions
  • Data quality
  • Downstream dependencies
  • Historical failure modes

The dangerous part is that load-bearing decisions often look ugly.

They can look like hacks. They can look outdated. They can look inefficient. They can look like technical debt.

Sometimes they are.

But sometimes they are the reason the system still works.

Why tests and reviews miss this

Code review is good at catching many things.

Readability.
Maintainability.
Obvious bugs.
Security issues.
Consistency with patterns.
Test coverage.

Tests are also useful. They can validate expected outputs. They can prevent regressions. They can document some behavior.

But tests and reviews often miss decision intent.

A test can tell you this input currently produces that output.

It may not tell you why that output matters.

A reviewer can tell you the code is cleaner.

They may not know which business decision changed.

An approval gate can tell you the deployment is safe.

It may not ask what behavior the system is protecting.

That is how a change can pass every technical check and still damage the business.

The missing question is not always, “Does this work?”

Sometimes the missing question is:

What decision changed, and do we understand it?

A migration example

During one migration, we were moving a core service.

At first, it looked like a normal software migration.

Move the logic.
Update the interface.
Validate the output.
Release the new version.

The team knew the code base. We had done migrations before.

This time, we used AI before touching the code.

Not to generate the replacement service.

Not to move faster.

We used it to map the decisions inside the old service.

The service looked like one unit of logic.

It was not.

It contained 31 distinct decisions accumulated over 8 years.

Some were still valid.

Some existed for a client that no longer existed.

Some compensated for a third-party bug that had already been fixed.

Without the map, we probably would have migrated all 31 decisions into the new system.

That would have meant copying old assumptions, obsolete rules, and unnecessary compensations into cleaner architecture.

With the map, we made a different choice.

We migrated 9 decisions.

The other 22 were documented, flagged for review, or scheduled for deliberate cleanup.

That changed the migration.

The value of AI in that moment was not code generation. It was decision discovery.

It helped us see what we were actually changing.

Using AI for decision discovery

Most AI-assisted engineering conversations focus on writing code.

That is useful, but it is not enough for legacy system modernization.

Before asking AI to generate replacement code, ask it to explain the decisions embedded in the current code.

Here are prompts I would use.

Prompt 1: Find business decisions

List every business decision this function appears to make. 
For each decision, explain the input, output, condition, and possible business meaning.
Do not rewrite the code yet.
Enter fullscreen mode Exit fullscreen mode

Prompt 2: Separate implementation from decision

Separate technical implementation details from business decisions in this code.
Group the findings into:
1. Pure implementation
2. Business rules
3. Data assumptions
4. Fallback behavior
5. External system dependencies
Enter fullscreen mode Exit fullscreen mode

Prompt 3: Find load-bearing decisions

Identify decisions in this code that may be load-bearing.
A load-bearing decision is behavior that may protect revenue, customer experience, operational flow, compliance, downstream systems, or rare edge cases.
Explain why each decision may be risky to change.
Enter fullscreen mode Exit fullscreen mode

Prompt 4: Analyze conditionals

Review every conditional branch in this code.
For each branch, explain:
- What decision it represents
- What input triggers it
- What behavior changes
- What might break if the branch is removed
Enter fullscreen mode Exit fullscreen mode

Prompt 5: Find fallback behavior

Find all fallback behavior in this code.
For each fallback, explain what failure, missing data, dependency issue, or historical constraint it may be protecting against.
Enter fullscreen mode Exit fullscreen mode

Prompt 6: Create a decision map

Create a decision map for this service.
For each decision, include:
- Decision name
- Inputs
- Output
- Business purpose
- Known assumptions
- Dependencies
- Risk if changed
- Suggested migration action
Enter fullscreen mode Exit fullscreen mode

Prompt 7: Identify obsolete rules

Identify rules that may depend on historical context.
Flag decisions that may be obsolete, client-specific, vendor-specific, or compensating for old system behavior.
Do not recommend deleting anything unless the reason is clear.
Enter fullscreen mode Exit fullscreen mode

The important part is the last sentence.

Do not let AI confidently delete behavior it does not understand.

Use it to surface questions. Use it to create a map. Use it to accelerate investigation.

The engineer still owns judgment.

A pre-refactor checklist

Before refactoring legacy code, I now want answers to these questions:

  • What decision does this code make?
  • What inputs influence the decision?
  • What output or behavior changes if I remove it?
  • Who depends on this behavior?
  • Is the rule still valid?
  • Is it protecting an edge case?
  • Is it compensating for another system?
  • Is it customer-specific, vendor-specific, or market-specific?
  • Do tests cover the decision or only the implementation?
  • Is the current behavior intentional or accidental?
  • What should be migrated, rewritten, deleted, or reviewed?

If you cannot answer these questions, you may still be able to refactor.

But you are refactoring with unknown risk.

That may be fine for low-impact code.

It is not fine for systems that make business-critical decisions.

A simple decision record format

When mapping legacy logic, I like to capture each decision in a lightweight format.

No heavy process. No large document. Just enough context to avoid losing intent again.

Decision:
Apply fallback routing when primary destination is unavailable.

Inputs:
Primary destination status, fallback configuration, request type.

Output:
A fallback destination is selected instead of failing the request.

Known context:
Originally added to prevent failed requests when the primary destination could not be used.

Risk if changed:
Requests may fail instead of being routed through a safe fallback path.

Owner:
Needs confirmation from service owner or business owner.

Migration action:
Migrate for now. Add observability. Review whether the fallback is still needed.
Enter fullscreen mode Exit fullscreen mode

This format forces the right conversation.

Not “Is this code ugly?”

But “What decision does this represent, and what should we do with it?”

What to migrate, rewrite, delete, or review

Once decisions are mapped, the migration becomes more deliberate.

I usually think in four buckets.

Migrate

The decision is still valid and clearly understood.

Keep the behavior. Improve the implementation if needed.

Rewrite

The decision is valid, but the implementation is outdated.

Preserve the intent. Change the code.

Delete

The decision is obsolete and the risk is understood.

Remove it deliberately. Add monitoring if the impact is not fully certain.

Review

The decision may matter, but the context is unclear.

Do not blindly migrate it. Do not blindly delete it. Document it, assign an owner, and review it.

This is where many migrations go wrong.

Teams often treat all old behavior as either something to copy or something to clean up.

Decision mapping gives you more options.

Where this fits in modernization

This applies to many types of work:

  • Refactoring legacy code
  • Monolith migration
  • Service extraction
  • API rewrites
  • AI code migration
  • Replatforming
  • Replacing vendor integrations
  • Simplifying business rules
  • Removing old feature flags
  • Consolidating duplicated logic
  • Reducing technical debt

In each case, the technical change is only part of the work.

The safer path is:

  1. Map decisions
  2. Recover intent
  3. Classify risk
  4. Decide migration action
  5. Refactor or rewrite
  6. Validate business behavior
  7. Document what changed

That sequence is slower than blindly rewriting code.

It is faster than breaking something important and spending months rediscovering why the old system worked.

AI is useful when it makes the invisible visible

I do not think AI removes the need for senior engineering judgment.

For legacy systems, I think the opposite is true.

AI makes senior judgment more valuable by surfacing the hidden logic that deserves attention.

It can help scan a large code base.
It can identify patterns.
It can group rules.
It can summarize decision paths.
It can find inconsistencies.
It can generate questions engineers should ask before changing behavior.

But AI should not be treated as the decision maker.

It should be treated as a decision discovery tool.

That shift matters.

The goal is not simply to migrate code faster.

The goal is to understand what the code is deciding before you migrate it.

The takeaway

Before you refactor legacy code, map the decisions hidden inside it.

Some of those decisions are obsolete. Some are accidental. Some are ugly. Some should be deleted.

But some are load-bearing.

They protect behavior that is not obvious from the code alone.

The safest modernization does not start with rewriting code.

It starts with recovering intent.

The unit of transformation is not the system.

It is the decision.


This article is adapted from my Medium essay and TEDx talk on decision recovery in legacy system modernization.

Medium article: https://medium.com/@nirmal.jingar/systems-are-not-made-of-code-they-are-made-of-decisions-4b7fa2ead212
TEDx talk: https://www.youtube.com/watch?v=pxUChqiyp2Y

Top comments (0)