Option maps over builders: a data-first API design
Builder patterns are a common way to handle configuration-heavy APIs, but in languages with good map literals, option maps are often the better choice. Instead of chaining method calls on a builder object:
Wizard wiz = new WizardBuilder("some string")
.withPriority(1)
.withMode(SOME_ENUM)
.enableFoo()
.disableBar()
.build();
a plain map keeps things simpler:
Wizard wiz = new Wizard("some string",
{:priority 1
:mode SOME_ENUM
:foo? true
:bar? false})
The strengths of option maps come from the fact that they are data, not executable code:
- They are shorter. With native map literals, the syntax is compact and there is no boilerplate for each field.
- They are serializable. Because an option map is just a data structure, you can read it from JSON, store it in a database, or send it over the wire. Builder APIs force you to write glue code that iterates over parsed data and calls the right setter for each key—a pattern that repeats endlessly.
- They avoid explicit freeze steps. Builders are usually mutable, which forces a separate build or freeze call. Option maps are already immutable, so there is nothing to finalize.
- They compose naturally. A function can take an option map, merge in defaults, transform some values, and pass it downstream. Composing builders requires a continuation or callback because you must yield the builder back to the caller mid-chain.
- They are order-independent by construction. Builder calls mutate state sequentially, so the order of options can inadvertently change behavior. Option maps have no implicit ordering, which makes compositions more reliable.
If option maps are so practical, why do builders dominate? The answer is largely static typing. Typical maps in mainstream languages are homogeneous (java.util.Map<String, Object>), and they allow any key with no per-key type guarantees. An option map may mix booleans, integers, and enums, and static type systems in languages like Java cannot express constraints such as “the :foo? key must be a boolean” or “the :mode key must be one of these three enum values.” Builders get around this by defining one method per option, each with a precise type signature.
That said, the static type advantage fades as soon as configuration is loaded at runtime from a file. You cannot statically verify the contents of an external config file, so type errors appear at runtime regardless. Builders only give you static validation for options that are hardcoded in your program source.
There is a way to view typed heterogeneous maps in Java: they are essentially objects. From that angle, a builder is just a verbose, statically typed option map.



