ConfigSource, ConfigClient, and ConfigChangeListener using the Observer pattern so components react to config changes without polling, plus fallback-to-default behavior when the config source is unreachable.
Published September 23, 2026
interface ConfigSource { Map<String, String> fetchAll(); }
interface ConfigChangeListener { void onChange(String key, String oldValue, String newValue); }
class ConfigClient {
ConfigSource source;
Map<String, String> cache = new ConcurrentHashMap<>();
List<ConfigChangeListener> listeners = new CopyOnWriteArrayList<>();
}
ConfigSource as an interface (rather than a hardcoded connection to one specific backend) is what lets the SAME ConfigClient work against different backing stores — a local file for tests, a remote config service in production — without any calling code changing, a direct application of Dependency Inversion.
class ConfigClient {
void registerListener(ConfigChangeListener listener) { listeners.add(listener); }
void refresh() {
Map<String, String> latest = source.fetchAll();
for (var entry : latest.entrySet()) {
String oldValue = cache.get(entry.getKey());
if (!Objects.equals(oldValue, entry.getValue())) {
cache.put(entry.getKey(), entry.getValue());
listeners.forEach(l -> l.onChange(entry.getKey(), oldValue, entry.getValue())); // notify subscribers
}
}
}
}
Components that care about a specific config value REGISTER as listeners once, then get notified automatically whenever that value actually changes — rather than each component independently polling ConfigClient.get("someKey") on every operation to check for a change. This is the same Observer-pattern benefit as Design a Feature Flag System's push-based invalidation: components stay decoupled from HOW/WHEN refresh happens, they just react when notified.
String get(String key, String defaultValue) {
return cache.getOrDefault(key, defaultValue); // last-known-good value, or an explicit default
}
If refresh() fails (the remote config source is temporarily unreachable), the client should keep serving the LAST SUCCESSFULLY FETCHED values from its cache rather than failing every config lookup — this is a direct application of Health Checks' essential-vs-non-essential reasoning: config staleness (serving slightly outdated values during an outage) is almost always preferable to config UNAVAILABILITY (every part of the application that reads config suddenly breaking). A genuinely never-yet-successfully-fetched key falls back to an explicit, caller-provided default rather than throwing.
Q: How does the client know WHEN to call refresh() — polling, or something else? A: Either a periodic poll (simplest, same freshness/latency trade-off as Design a Feature Flag System) or a push-based mechanism (the config source notifying the client of a change via a webhook or a long-lived connection) — the Observer pattern here is about how CHANGES PROPAGATE TO INTERNAL LISTENERS once detected, independent of whether detection itself is poll-based or push-based.
Q: Is there a risk in notifying listeners synchronously, inline within refresh()? A: Yes — a slow or misbehaving listener could block the entire refresh cycle (and delay other listeners from being notified); a more robust implementation dispatches notifications asynchronously (each listener notified on its own thread/task) so one slow listener doesn't degrade the whole notification pipeline.
Q: How would you avoid every application instance hammering the config source with its own independent polling? A: At real scale, a common pattern layers a caching/pub-sub tier BETWEEN the config source and many client instances — instances subscribe to that intermediate tier rather than each polling the ultimate source directly, similar in spirit to how a CDN sits between many viewers and one origin server.
Q: What's a concrete failure mode if listeners aren't properly unregistered when a component shuts down? A: A classic memory leak — the listener list holds a reference to the component indefinitely, preventing garbage collection even after the component is logically done, the same lifecycle-management discipline as MDC/ThreadLocal cleanup covered in Centralized Logging, applied to a different kind of resource.