Maps
I don't expect to see Map<String, Object> around a codebase very often these days. Maybe around legacy low-level code, like JDBC or HttpURLConnection code, or an implementation of some complicated formula. But definitely not in modern application code for a web application or web service. Developers generally model their data with domain objects or data transfer objects.
However, sometimes I still come across projects where Maps are used rampantly! Here's an example from my most recent project:

Why do this?
There are reasons teams end up here, and some of them are legitimate:
- It allows the application to be completely agnostic as to what data passes through it. This could be worthwhile in some situations, such as if the backend uses unstructured data storage.
- It allows code to be more extensible, enabling a single API endpoint to serve different clients with different request bodies.
Why is it a problem?
- You can't tell what data structure a Map<String, Object> has by just looking at it.
- The data hasn't been classified, so you can't easily see how it is used throughout the project.
- It's more difficult for developers to discover ways to improve the application based on what the code "speaks" to them (what Kent Beck calls "The adjacent possible.")
- You have to write more tests to confirm that your Maps contain the structure of data you expect (which is something that could be confirmed for you at compile time).
What can we do?
The preferable option is to rip off the band aid.
If you know the shape of the data, you can use IDE tools (for example, IntelliJ's RoboPojoGenerator) to generate classes that model your data, then refactor everything associated with it.
If this is impossible to do all at once, there is another option.
Another option is to make incremental changes
If you can't take on the refactor all at once, you can still incrementally implement a change that preserves all the functionality and the flow of the current state of the code while giving yourself the classification you desire.
You can create your own class that implements Map, and beef it up however you like.
You could create an abstract MapWrapper class that wraps around your normal Map. Then, you can make DTOs and Domain Objects that extend from MapWrapper, and include getters and setters to model the structure of the data.


Here's how the Controller code looks now:

Why this works
MapWrapper implements Map, so any method in your code that accepts a Map will also accept a MapWrapper. That means you can selectively update method parameters, method return types, local variables, and class-level variables from Map to MapWrapper wherever you would like (wherever the ongoing project work is); and leave everything else intact.
Advantages
- You don't have to change all of the Maps to MapWrapper, so that spares you from having to touch fragile legacy code.
- You've taken another step closer to modeling the data.
- You can clear up application code that drills into maps and improve the signal-to-noise ratio.
Disadvantages
- Your application is now "in-between" techniques of modeling, where some of the data is somewhat modeled and used in some parts of the code.
- This is a somewhat strange and confusing pattern for someone who hasn't seen it before. It is likely to be misunderstood and abused in ways that I could never imagine.
Final thoughts
Map<String, Object> has its place, but modern application code is rarely that place.
When the data has meaning, the code should reflect that meaning. Types are not bureaucracy; they are communication. If you can model your data, do it. If you can’t do it all today, take a step that makes tomorrow easier.
Even small improvements in classification and intent can dramatically improve the health of a codebase over time.



