One of the most consistent contributors to success in the projects I’ve led has been the strategic use of design patterns. Not the textbook kind that you memorize for interviews — but the kind you reach for when the legacy code is brittle, dependencies are unclear, and timelines don’t allow for clean rewrites.
My favorite? The Strangler Pattern — and the sub-patterns it enables.
Let me share one of the most rewarding times I used it: leading a redesign of a high-volume B2B API in the FinTech space. The API had to evolve, but we couldn’t afford to break any existing behavior. We weren’t sure who used which fields, or what would happen if we changed them. In other words: classic legacy.
And this is exactly where good design patterns shine. They give you a safe, structured way to modernize systems without stopping the world.
A Simple Example: Legacy Data You Can’t Touch
Imagine this: you’re working on an API where user records look like this.
{
"first_name": "John",
"last_name": "Smith",
"address": {
"street": "Platz der Republik 1",
"city": "Berlin",
"country": "Germany"
}
}
You’re asked to support multiple addresses per user. A simple task on paper — change the schema, migrate the data, update the code.
But what if another internal service depends on that address field exactly as it is? What if that integration is undocumented, or outside of your team’s control? You can’t afford to break it, and you don’t have time to refactor every downstream consumer.
This is where I lean on the Strangler Pattern, and more specifically, the Proxy Pattern.
Containing the Mess: A Proxy for Compatibility
Instead of breaking the existing shape of the user object, we introduced a new addresses collection:
{
"user_id": "123",
"is_primary": true,
"street": "string",
"city": "string",
"country": "string"
}
And we built a proxy that acted as a safe bridge between the old world and the new one:
function addAddress(user, address) {
if (address.is_primary) {
user.address = {
street: address.street,
city: address.city,
country: address.country
}
// save user
}
// save address to addresses collection
}
function setAddress(user, address) {
return addAddress(user, {
user_id: user.id,
is_primary: true,
...address
});
}
function getUser(id) {
// return the user — unchanged from legacy structure
}
The legacy setAddress
and getUser
methods still work exactly as expected. But behind the scenes, we’re slowly moving toward a better model.
Any time a new primary address is added, we update the user.address
field. Other addresses live in the new collection. We keep everything backward-compatible, while the new functionality lives safely in parallel.
Why This Works
We didn’t just write new code — we contained the mess. We isolated the legacy logic, introduced a better model alongside it, and made sure the two could live together. That’s the essence of the Strangler Pattern: grow the new system around the old one, and slowly replace it piece by piece.
You don’t always have the luxury of refactoring everything. Sometimes you have to work with constraints — unknown touchpoints, undocumented dependencies, and tight timelines. But that doesn’t mean you need to compromise on architecture. You just need to be more strategic.
Creating a proxy layer is one of the best tools I’ve found for safely evolving systems. It gives you room to move, without breaking what’s already working.
And eventually, once you’re confident no consumers rely on the old field, you can remove it — without needing to touch the rest of your implementation at all.