Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
“Future proof your architecture” sounds good. But the reality is you can’t future-proof Software Architecture. When you really think about it, the future is just what’s breaking assumptions. You can’t really future-proof that.
What you can do is contain changes so they don’t ripple through your system.
Where people go wrong is trying to future-proof with abstractions everywhere. What you really want to be doing is controlling the blast radius, meaning controlling where change goes.
YouTube
Check out my YouTube channel, where I post all kinds of content on Software Architecture & Design, including this video showing everything in this post.
I’m going to explain this using a thread by Aaron and elaborate on some of the things he’s pointing out.
He posted:
I posted a lot of bangers about SDK bin’s terrible software choices and how it generally made life unpleasant for us. So I wanted to detail how we’re addressing the dumbest and worst design choices in the codebase.
So the first one: what did our dev do when we needed to renew an annual subscription? Modify the subscription created date and reset the renewal reminder hard coded as N months from the creation date.
Now you might be thinking, “That’s ridiculous. I would never do that.”
But it underlines why things change.
The Unknown Is Usually Boring Stuff That Stops Being Boring
In the context of future proofing, the unknown usually lies in things like:
- pricing rules changing
- renewals and billing schedules changing
- tax changes
- refunds and how those show up
- partial payments
- a new payment provider, or a second payment provider/gateway
- workflow that used to be simple, becoming not so simple
That’s the real problem. Early on, when you’re building a system, you can have a lot of assumptions about the unknown.
What kills you is early decisions that leak everywhere in your codebase and cause coupling.
You have assumptions. You make decisions. Those decisions leak everywhere. Now you’re coupled. And that coupling is going to cause a lot of pain later when you try to make change.
You know you have this problem because when a change comes in, you have to touch all these things:
- the UI
- the “domain”
- persistence
- some random shared helpers and utils
- reporting
- background jobs
- and my favorite: three or five or a dozen other places you didn’t even know existed
You’ll often hear, “Well, we have a very large system and it’s very complex.”
In the context of what I’m talking about, that’s not complexity. That’s coupling.
Stripe Isn’t the Problem. Leaking Stripe Everywhere Is.
In Aaron’s case, he’s feeling the pain of everything being coupled so tightly to Stripe that it’s taken a mini Manhattan project to move off it.

Related, sure, but fundamentally separate concerns. So the assumptions and unknowns causing pain here are exactly what I said at the beginning:
- the idea of a renewal wasn’t a first class concept
- moving off Stripe becomes a disaster
- conflating invoices and payments creates more pain
That “Created Date Renewal Hack” Is a Symptom
Here’s the type of thing that happens when the Stripe assumption leaks into your system.
You end up with leaked information in your subscription, and who knows where else, like:
- Stripe customer ID
- Stripe invoice ID
And then the only real concept you had was a created date time.
But because you needed renewals after the fact, you didn’t model it.
So that created date turns into a hack. You “renew” by pretending it started again. You overwrite the created date and reset the reminder hard coded off that date.
This is also one of those situations where if the business actually knew you were overwriting this data, you’re potentially losing a lot of valuable info. Audit info. What actually happened. When did it renew. What was the history.
If you talk to someone non technical in the business, whether they care about that, they’ll probably say yes.
Your Data Model Isn’t Your Domain Model
This is where I’ll say something you’ve heard me say before:
Your data model isn’t your domain model.
How you persist data, what your schema looks like, that’s not your domain model.
If you look at your model and think, “This doesn’t really express the domain,” then yeah, you probably don’t have a domain model. You have a data model. A bucket of data. Getters and setters.
Bonus tip: it’s also not your resource model.
You have an HTTP API. Those are different things.
What you return to clients isn’t your underlying schema and isn’t your domain model. It’s a representation of what you want to show to clients.
So What’s the Fix? Not Interfaces Everywhere.
So what’s the fix?
In the Stripe example, it was coupled everywhere.
Is the fix immediately to jump to interfaces and put them everywhere? Use the adapter pattern everywhere all the time?
No.
It’s what I said at the beginning: controlling the blast radius. When you make a change, it should be localized to one particular place.
With Stripe specifically, it depends how coupled you are:
- Is the Stripe customer ID leaked into your database?
- Are other clients using it because you exposed it via your HTTP API?
- Do other libraries use it?
- Are you using the Stripe SDK in 10 places, or 200?
If you have 200 usages, and it’s all through the UI, the domain layer, persistence, reporting, background jobs, and everywhere else… you don’t have a blast radius.
You have a disaster.
Separate Concepts: Invoice vs Payment
Another simple example is the invoice vs payment problem.
They were treated as one concept and it was a disaster.
They don’t need to be. They should be separate things. You can have an invoice that has nothing to do with Stripe. It’s just an invoice. No Stripe internals. It’s your concept. Then payments are a separate concept.
Now you can apply payments to an invoice. Partial payments? Fine. Refunds? That has nothing to do with invoices. That has everything to do with payments.
Separate concern. Easier to support. Easier to change.
Watch Your Nouns: Third Party Vocabulary Leaks
Pro tip: when you’re using third party services heavily, especially if they matter a lot to you, the nomenclature from that third party starts leaking into the core of your system.
You’ve got to be careful there.
You want your product’s nouns and verbiage to be yours, not the third party’s.
A Concrete Blast Radius Example
Here’s what “controlling the blast radius” can look like when paying an invoice.
You fetch the invoice from the database.
You create a payment. Separate concept. You call Stripe to charge the account. If it succeeds, you mark the payment as succeeded. If it fails, you mark it as failed.
Then you save your database changes.
Yes, you’re going to have reconciliation, because if something fails on your side but the charge actually went through, you deal with that after the fact.
But the point is this: You’re controlling the blast radius of where you deal with Stripe. It’s concrete. It’s real. But it’s contained.
If you need to change payment providers, you change it where you have that capability exposed. It’s not coupled everywhere.
How People Make It Worse
This is where people take the right problem and make it worse.
Before you say, “Let’s make an IPaymentProvider used across the whole system” — congratulations, you just created shared coupling.
Or “Let’s build a generic billing framework” — no, you created a framework specific to one implementation that isn’t generic at all.
“Let’s reuse this shared library across services or slices” — no, what you created is a distributed monolith starter kit.
Designing for the Unknown
So how do you design and architect for the unknown?
It’s not about trying to future-proof Software Architecture. It’s about containing the blast radius.
For the things that often change — rules, workflows, integrations — if you segregate them, you can change them without it affecting your entire system.
Where things go wrong, like the Stripe example, is leaking internal information throughout the system. Then if it changes… now what?
Because you didn’t localize it. It permeated everywhere, and the blast radius is huge.
Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.