Safe Changes
Learn how to refactor legacy systems safely using characterization tests and Consumer-Driven Contracts (CDC) that lock in existing behavior and prevent breaking changes.
We'll cover the following...
Legacy systems are easy to break and hard to reason about, which means they slow everything down (even more than John does).
At the Staff+ level, it’s your responsibility to make change safe across your org’s systems and teams.
That’s where characterization tests and Consumer-Driven Contracts (CDCs) come in.
These tools let you:
Lock in legacy behavior before refactoring.
Catch breaking changes before they hit production.
Reduce “fear of change” in codebases that others won’t touch.
These tools are your keys to modernizing systems, managing blast radius, and enabling other teams to move faster.
Characterization tests
Characterization tests capture the existing behavior of code. Instead of verifying “correctness,” they define what the code currently does so it can be refactored confidently.
These are especially useful when working with:
Legacy code
Poorly documented modules
Systems with no existing test coverage
Example: Legacy order processing module
Imagine you get asked to clean up a legacy order processing module. It’s messy, untested, and hasn’t been touched in three years. You spot redundant logic and want to simplify it, but how do you know you won’t break something?
Before you refactor, lock in the current behavior with tests, as follows:
def test_order_discount_behavior():order = Order(user_type='gold', total=100)assert order.calculate_discount() == 15 # not documented, but this is what it returns today
Now you can safely refactor calculate_discount() while preserving its externally visible behavior.
Consumer-Driven Contracts (CDC)
In a microservices architecture, CDCs ensure that service providers (APIs) honor the expectations of their consumers (clients). Instead of relying on assumptions or documentation, consumers publish contracts describing how they use the API.
Tooling Tip: Tools like Pact, Spring Cloud Contract, and Hoverfly help implement CDC in practice.
Example: Invoicing service
The payments team removes a field tax_rate from their API. They assume no one uses it. But the invoicing service breaks in production because it relies on tax_rate. This wasn’t just a bad change; it was an unsafe one.
The invoicing service publishes a Pact contract:
{"request": {"method": "GET","path": "/payment/summary/123"},"response": {"status": 200,"body": {"amount": 120,"tax_rate": 0.15}}}
Before deploying any change, the payments team runs a provider verification test that checks if they still satisfy this contract. If they remove tax_rate, the test fails, and the pipeline blocks.
This means:
Consumers get notified early.
Breaking changes are caught before prod.
Service teams are aligned through automation, not 3 hour long meetings.
John Quest: Characterization test
You’ve inherited this legacy function:
def compute_fees(amount, user_type):if user_type == 'premium':return amount * 0.02return amount * 0.05# Hardcoded valuesamount = 1000user_type = 'premium'# Compute feefee = compute_fees(amount, user_type)# Correct print syntaxprint(f"User Type: {user_type}")print(f"Amount: {amount}")print(f"Transaction Fee: {fee:.2f}")
You don’t know why the percentages are what they are. Also, no one knows why this behavior exists. Like any self-respecting Staff+ (even John) would, you want to refactor it.
Task:
Write a test that freezes its current behavior.
To check your answer, click the “Solution” button.
You can now safely rename variables, restructure logic, or introduce constants without changing behavior.
Test your knowledge
Quiz: Staff+ Safe Changes
What is the purpose of characterization testing?
To assert the correct business logic
To test new features
To document current behavior for safe refactoring
To detect race conditions