PUT is all you need
Working solutions to past software design problems can almost never be applied to new ones without adjustment. The amount of context is so big that it would break the largest LLM's context window. But we still try anyway. I am no exception. I have tried to hammer new nails with old methods. Which is why I have come to cherish having the time to reflect on my work - sometimes with the help of a random comment from a colleague. Especially precious are those times when I arrive at a totally different solution, one that I might have found deeply objectionable before. This is a story about one of those times.
A little PUT endpoint never hurt nobody
Imagine you are building software for controlling temperature sensors. Your newest feature deals with configuration (who doesn't love some good configuration?). Your users request a whole host of configuration options. You decide to keep it simple and make a PUT endpoint that replaces the whole configuration with each request.
The frontend folks are happy: they can do whatever they want (a big-ass form with a save button) and then just PUT the changed configuration. The API clients are happy: there is just one endpoint they have to integrate. Everything looks fine until...
The configuration gets more complicated. Temperature threshold options now depend on the selected sample interval! You cringe. How do you know if those settings have even changed? You sigh and just add the validation to every call of the PUT endpoint. But then it gets worse: your customers demand that you send an email when a particular set of parameters changes. You cringe again. Fine. You implement a delta computation that spits out what actually changed, and add a check to evaluate whether you need to send the email. Already, the separate checks interact: you should not send an email if the configuration is invalid in the first place.
Imagine several more validations and side-effects your endpoint has to support. Some validations might even require additional data that you need to fetch conditionally. What does the code look like? Typically you would expect full-on spaghetti, noodles coming out of every pore. In the best case, there is a structured translation of the incoming payload into finer-grained deltas, so you know what validations to execute, what additional data to fetch, and what side-effects to trigger. That structure is certainly still intimidating for every newcomer and hard to change.
You might argue that there is a clean way to implement what I described - maybe create a bunch of objects that you orchestrate - but in my experience, those objects are just the meatballs on top of the existing mess (using a more functional approach does not change the situation, just the food metaphor). The problem is not the lack of elite-level software engineering skills. The problem is that the solution does not match the context.
Operations as endpoints
The users (or customers, subject matter experts, stakeholders...) in our scenario clearly care about the exact changes to the configuration, not just that a new version is available. The API does not reflect that. Should it? My knee-jerk reaction is to jump back in time to day one and advise the developer on duty to split the relevant operations into separate endpoints. Immediately, each of these endpoints will be much simpler: it will be obvious what validation is needed, what extra data to fetch, and what side-effects to trigger. Newcomers can clearly see the set of operations we support by looking at the endpoints, and each one will be much easier to understand. If this is the outcome, we can agree it would be wonderful - whether or not you'd have picked the same approach. However, I have a confession to make. What really happened was that I started with multiple endpoints and ended with one PUT to rule them all.
Let's jump back to the start. Imagine that complex validations never arrive and the side-effects stay general in nature (triggered if anything changes). In other words: what if no one actually cares about the details of each change request, just that it happened? What happens if we implement the fine-grained version? The first thing that happens is that the frontend folks start to yell at you. "Why do I need to call all these endpoints?" Turns out the UX is built upon making many changes before saving. The API clients also yell at you. "Why do I need to learn all these endpoints?"
While you drown out the yelling with some good music, you realize that the small-operations approach also creates problems for you. That expensive side-effect that is triggered if "anything" changes? Well, "anything" changes all the time now. You need to create a workaround. You cringe. Why did this happen? All you wanted to do was expose some meaningful operations.
Meaningful operations
My engineering bias is to explicitly model all actions that can be taken in a system. I strongly believe it is important to understand what users want to do, but also provide guardrails for how they do it. This is a consequence of the complex safety-impacting workflows I have implemented over my career. No wonder I imagined all this complexity crashing down on our little PUT endpoint. A fine-grained solution seemed obvious, but when the complexity never arrived, I realized I had made the same category error as the developer in the PUT version of the story. The solution did not match the context.
Looking back at the operations I exposed as endpoints, how did I know they were actually supporting a meaningful process? I did not. Instead, the change operations I mapped to endpoints were just helpful examples for myself to understand the problem space better. The real change operation was the simple replace request that I had been trying to avoid. None of my users cared about how changes happened, just that they happened. This was meaningful to them, even though my instincts around complexity told me otherwise. Sometimes less specificity is the meaningful choice. Sometimes PUT is all you need.
Conclusion
The story is incomplete and it cannot have a clear takeaway. Instead this is one of those precious moments I mentioned at the beginning: a time for you to reflect on your own work and experience. What is your bias when building software? Model the domain on every level or slap a PUT on it and be done? Do you have enough context to make this decision? A good first step is to think about what is actually meaningful to your users - not just to us.