I had a good example of this in a system used across multiple physical locations. Each location had its own local server. Gaming computers inside the location continuously polled that server and sent data to it. The local server aggregated information and communicated with a central cloud server.
At first glance, there were several things I didn’t like. There was a server that had to be deployed and maintained at every location. Logic existed on several levels. Clients were continuously polling the local server, and the whole communication flow was more complicated than I thought it needed to be.
But that doesn’t mean the original architecture was wrong.
The original architecture solved a real problem
The system had originally been designed around an important requirement: each location should be able to operate autonomously. The local server made that possible. Even without a permanent connection to the central cloud environment, the computers at that location still had local infrastructure they could communicate with.
So I don’t think it would be fair to look at the architecture several years later and simply say: polling is bad, local servers are unnecessary, this should use WebSockets. The original developers were solving a different problem.
Polling itself wasn’t the problem
I would still deliberately use polling today in the right situation. If the server doesn’t need to proactively send information to a client, and the client only needs to periodically retrieve or report status, polling can be a perfectly reasonable solution. It is simple and easy to understand, and sometimes a status update every few seconds or minutes is all the product actually needs.
In our case, however, communication needed to become genuinely bidirectional. The computers were sending information to the server, but the server also needed to send commands back to specific computers.
The important question was not “which technology is better?”
The question was: what kind of communication does the system actually need now? If we only needed periodic status updates, I would probably have kept the simpler model. But the new business functionality required two-way real-time communication between specific computers and the server. That made WebSockets a logical choice in this particular case.
This distinction matters because architectural decisions are often discussed as if technologies formed a hierarchy.
Future scale matters, but I don’t design for abstract “100× growth”
There was another reason to make the change before the system had actually failed. Management had concrete plans to increase the number of locations and devices, so this wasn’t a theoretical discussion. We already knew the direction the business wanted to take. I prefer that kind of information to statements like “we need something that can scale 100×”. I would rather know:
Those are architectural requirements I can work with. An abstract requirement to “make it scalable” can justify almost any amount of unnecessary engineering.
Migration complexity also belongs in the decision
Moving away from the existing model required changing a significant part of the system, so there was a real migration cost. But there was another interesting result: once the migration was complete, the new architecture was easier to understand and maintain than the previous two-level model with local servers.
In this case, we accepted a significant one-time change in exchange for a simpler long-term model that also supported the new requirements.
We validated the change according to the actual risk
We didn’t have hundreds of locations at the time, so I didn’t think we needed an elaborate multi-stage production rollout simply because that is what a theoretical deployment playbook might recommend. We first tested the new communication model locally on a limited number of computers, checked that connections remained stable and that we weren’t seeing errors or losing data. Once the system behaved reliably, we deployed the change across the existing locations.
We also didn’t collect extensive before-and-after benchmarks. The existing load was still relatively low, so such numbers would not have told us very much. If the system had already been operating under significant load, I would have wanted objective measurements — request rate, latency and connection stability — before making conclusions. The validation method should match the actual situation.
Best practices are a starting point
I treat architectural best practices as accumulated experience, not as rules that replace thinking. Someone has seen a pattern create problems repeatedly and turned that experience into a recommendation. That’s useful. But the recommendation doesn’t know anything about the particular system I’m looking at.
In this case, “polling is bad, use events instead” would have been too simplistic. The original architecture solved the autonomy requirement it was designed for, and replacing it without understanding that context would have been a mistake. But keeping it simply because it had worked for years would also have been a mistake once the requirements changed.
My rule for existing architecture
Not every old decision needs correcting. But when a concrete business requirement makes the limitation predictable, I would rather address it proactively than wait for downtime, data loss or operational problems to prove something we could already see coming. Understand why the system behaves the way it does before changing it.
Tell us what the system was built for and where the business is going. We’ll say whether a change is worth the migration cost.
Request a review →