Generalizing Features: The Bulk Rename Story
What it took to turn a single-purpose bulk-rename tool into something reusable across device types, without overbuilding it, and the trade-off that came with generalizing.
Part 7 of 10 in Building MistPulse: A Network Architect's Field Notes

A lot of software advice pushes you toward generalizing early: build the abstraction up front, anticipate the second use case before you've even shipped the first. My experience building MistPulse pushed me toward the opposite instinct, and I think the opposite instinct was right for a solo project: build the narrow, specific thing first, ship it, let it prove itself in real use, and only generalize once a second, genuinely similar need shows up. The bulk device rename feature is the clearest example of that pattern in this entire app.
Starting narrow, on purpose
The first version of this feature did exactly one thing: let me select a batch of access points at a site and rename them according to a pattern, in one action, instead of clicking into each AP individually. It was built with access points specifically in mind: the data model, the validation rules, even some of the UI copy assumed "this is always an AP." That narrowness wasn't a mistake; it was the right amount of scope for a first version, because it let the feature ship quickly and get real use before any abstraction decisions had to be made.
Using it for a few weeks confirmed the feature was genuinely useful, useful enough that the obvious next question became "why can't I do this for switches too?" Switches get renamed in bulk for the same reasons APs do: standardizing naming conventions, cleaning up after a site refresh, correcting an inconsistent legacy naming scheme. The operational need was identical. The underlying device data was not.
Build the narrow thing first. Let it prove itself. Generalize only once a second, genuinely similar need shows up.

What actually had to change
The naive move would have been to duplicate the component wholesale: copy the AP rename modal, swap in switch-specific labels and API calls, and ship two near-identical files. That's fast in the short term and expensive forever after: every future bug fix or UI tweak would need to be applied twice, and the two copies would inevitably drift out of sync over time. Instead, I generalized the component to accept a device type as a parameter and branch its behavior, validation rules, field mappings, the specific API calls it makes, based on that type, while keeping the shared parts (the multi-select UI, the pattern-based renaming logic, the confirmation flow) genuinely shared.
This is where the bug-sweep habit from Part 4 earned its keep again. Generalizing a component means re-examining every assumption the original, narrower version quietly relied on. Some fields that were always present on an AP response weren't always present on a switch response. Some validation that made sense for AP naming conventions didn't translate directly to how switches are typically named in a real deployment. Each of those had to be found deliberately, by comparing real API responses for both device types side by side, rather than discovered by accident after shipping.
- Ship the narrow version first, and let real usage validate that generalizing is worth the cost
- Generalize by parameterizing shared logic, not by duplicating files with small edits
- Every assumption the narrow version made needs to be re-checked against the new use case's real data
- A shared component with a device-type parameter stays maintainable; two near-identical copies do not
The trade-off I accepted
Generalizing added real complexity to a component that used to be simple to reason about. The rename modal now has to branch on device type in several places, and understanding its full behavior means understanding both branches, not just one. I accepted that trade-off deliberately, because the alternative, permanent duplication, is a worse long-term cost for a codebase that one person has to maintain indefinitely. But it's worth naming honestly: generalization isn't free, and the right call is to pay that cost only when a second real use case has actually shown up, not preemptively.
This same pattern, narrow first, generalize on proof, verify every carried-over assumption, has repeated itself in smaller ways across other parts of MistPulse since. It's become one of the clearest lessons from this whole project, and it's one I'll return to directly in the retrospective post later in this series.
A pattern worth naming
If there's a takeaway for anyone building a similar tool solo, it's this: resist the urge to build the general version first because it "seems more correct." The general version you'd guess at before a second use case exists is very often wrong in ways you can't predict, because you're generalizing from imagination rather than from two real, concrete cases you can compare side by side. Ship narrow, prove the value, then generalize deliberately with real data in front of you.
Everything covered so far: the proxy architecture, the API discipline, the Siri integration, the feature generalization, sits on top of a security posture I haven't yet described in full. The next post pulls that together: the specific security decisions baked into MistPulse, and why a cybersecurity background changed how nearly every feature got built.
Coming up next: Security by Design: encryption, redaction, and least privilege as first-class features, not afterthoughts
Gallery


Building MistPulse: A Network Architect's Field Notes
- 1.The Idea: Why I Built MistPulse
- 2.Laying the Foundation: Architecture and the Security-First Proxy
- 3.Curl First, Build Second: The API Validation Methodology
- 4.Real Bugs, Real Fixes: Debugging in the Open
- 5.Teaching Siri to Speak Mist
- 6.The Entity Resolution Saga
- 7.Generalizing Features: The Bulk Rename Story (you are here)
- 8.Security by Design
- 9.When APIs Change Under You: The SLE Migration Story
- 10.Lessons Learned & What's Next
Related posts

Teaching Siri to Speak Mist
Building native App Intents integration for MistPulse: a parallel Swift API client, its own build line, and the real TestFlight failures that came with going native.

When APIs Change Under You: The SLE Migration Story
A real API deprecation, a real deadline, and the verification methodology that made migrating MistPulse's SLE metrics endpoint boring instead of risky.

Security by Design
Encryption, redaction, and least privilege as first-class features in MistPulse, not afterthoughts, and why recurring security review matters more than a one-time audit.

The Entity Resolution Saga
Getting a spoken name to reliably match the right site or device in Mist: why exact matching failed, and how fuzzy matching with a confidence threshold fixed it.
Comments
No comments yet. Be the first.