The setup
I was running a system where multiple backend servers each served a set of users, and a routing layer decided which server a given user's requests should go to. Nothing unusual this is a common shape for anything that scales past one machine.
The question that turned out to matter a lot more than I expected: when something in the routing layer refers to "the server," what is it actually storing? A real address? Or a name that gets looked up?
The moment it mattered
At some point I needed to move a group of users off one server and onto another the kind of thing that happens for all sorts of reasons: retiring old hardware, rebalancing load, recovering from an incident.
Before touching anything, I checked how the routing data was actually structured. It turned out every record referenced servers by a stable label not a raw address. The label was just an ID; a separate lookup table mapped each label to wherever that server actually lived right now.
That one fact changed the entire migration from "rewrite a bunch of records for every affected user" into "update one row in the lookup table." The routing records themselves never needed to change. Every user still pointed at the same label the label's meaning just changed.
I verified it worked by checking the routing behavior for an affected user right after the update, before assuming anything.
The pattern, generalized
This is the indirection pattern, and it's worth naming explicitly because it's easy to skip when you're building the first version of something:
- Direct reference: Store the real, resolvable thing (an IP address, a file path, a specific resource ID) everywhere it's used.
- Indirect reference: Store a stable name/label everywhere it's used, and keep exactly one place that resolves that label to the real thing.
Direct references feel simpler at first there's no lookup step, nothing extra to maintain. But that simplicity is a trap: the real address ends up copied into every place that uses it, and moving the real thing means finding and updating every one of those places.
Indirect references cost you one extra lookup table. In exchange, moving the real thing becomes a single update, in a single place, and everything downstream is unaffected because none of it ever knew the real address to begin with.
Where this shows up beyond servers
The same trade-off appears constantly:
- DNS names vs. hardcoded IPs this is the same pattern at internet scale. Change the DNS record, nothing downstream needs to know.
- Feature flags referencing a config key vs. a hardcoded value the flag's name stays stable while what it resolves to changes.
- Database foreign keys vs. duplicating a denormalized value everywhere the ID stays stable; look up the current details when you need them.
- Service discovery vs. hardcoded service addresses exactly the migration scenario above, generalized to any service-to-service call.
In every case, the question is the same: if the "real thing" changes, how many places need to be touched? If the answer is "more than one," you're using a direct reference somewhere it should have been indirect.
The lesson
Before building the first version of anything that will eventually need to move, scale, or be replaced a server, a config value, a downstream dependency decide up front whether callers will reference it directly or through a label that gets resolved. It's a small design decision early on, and it's the single biggest factor in whether a future migration is a five-minute change or a project.
If you're not sure whether you have this in place already, ask the question I asked before touching anything: what is actually stored in the place that decides where a request goes a real address, or a name? If it's a real address, that's worth fixing before you need to move it under pressure.
Top comments (0)