Monostate Pattern: Shared Behavior Without a Single Instance
The Monostate (Borg) pattern shares state across every instance via static fields instead of restricting instantiation — an under-taught alternative to Singleton that plays better with inheritance and polymorphism.
Monostate Pattern
Monostate — sometimes called the "Borg" pattern, after a Python idiom popularized by Alex Martelli — achieves the same practical outcome as Singleton (all callers observe and mutate the same shared state) through the opposite mechanism: instead of restricting how many objects exist, it lets you construct as many objects as you like, and simply makes their state shared via static fields. Every instance is behaviorally identical because they're all fronting the same underlying data.
1. Intent
Monostate answers a narrower question than Singleton: not "how do I guarantee only one object exists," but "how do I guarantee every object behaves identically, without callers having to know they're dealing with something special?" It moves the sharing from the instance level (Singleton: one object, referenced globally) to the state level (Monostate: many objects, one underlying data store).
This distinction matters in practice because callers of a Monostate class write completely ordinary object-oriented code — new Config(), pass it around, store it as a field — and get shared-state semantics for free, with no getInstance() call anywhere in the codebase.
2. The Naive Approach and Why Monostate Exists
Suppose a PrinterSettings class needs one shared paper size and orientation across the whole application, but the class is used throughout a large legacy codebase via ordinary new PrinterSettings() calls scattered across dozens of files — refactoring every call site to PrinterSettings.getInstance() would be a large, risky change.
// VIOLATION: each PrinterSettings instance has its OWN state —
// setting the paper size in one place doesn't affect any other instance.
class PrinterSettings {
private String paperSize = "A4";
private String orientation = "PORTRAIT";
void setPaperSize(String size) { this.paperSize = size; }
String getPaperSize() { return paperSize; }
}
// main() somewhere:
PrinterSettings userPrefs = new PrinterSettings();
userPrefs.setPaperSize("LETTER");
// ... deep in another module, constructed independently:
PrinterSettings printJob = new PrinterSettings();
System.out.println(printJob.getPaperSize()); // "A4" — the LETTER change is invisible here!Converting this to Singleton would require rewriting every new PrinterSettings() call site into PrinterSettings.getInstance() — a large mechanical change across the codebase, and one that changes the type's public API shape (adds a static factory method, typically also requires making the constructor private, which breaks any code doing new PrinterSettings() directly). Monostate fixes the sharing without touching any call site.
3. Structure
Note what's absent compared to Singleton's structure: no private constructor, no getInstance(), no instance-count enforcement at all. The only change from an ordinary class is that instance fields become static fields, and the accessor/mutator methods remain ordinary instance methods that happen to read/write shared static storage.
4. Java Implementation
class PrinterSettings {
// static — this is the ONLY change from a normal class: state lives at
// the class level, not the instance level.
private static String paperSize = "A4";
private static String orientation = "PORTRAIT";
// Constructor is public and completely ordinary — callers don't need
// to know anything special is going on.
public PrinterSettings() { }
public void setPaperSize(String size) { paperSize = size; }
public String getPaperSize() { return paperSize; }
public void setOrientation(String orientation) {
PrinterSettings.orientation = orientation;
}
public String getOrientation() { return orientation; }
}// FIXED: any number of instances, all observing the same shared state
PrinterSettings userPrefs = new PrinterSettings();
userPrefs.setPaperSize("LETTER");
PrinterSettings printJob = new PrinterSettings(); // a genuinely different object
System.out.println(printJob.getPaperSize()); // "LETTER" — shared state, as intended
System.out.println(userPrefs == printJob); // false — they are NOT the same object,
// unlike Singleton, where this would be trueThis is exactly the "common coupling" anti-pattern flagged in the Coupling & Cohesion guide when it happens accidentally via stray static fields. Monostate is that same mechanism used deliberately and explicitly, as the entire documented contract of the class — the difference between a bug and a pattern is intent and visibility, not the underlying mechanism.
Thread Safety Still Applies
Monostate doesn't sidestep concurrency concerns — it has exactly the same shared-mutable-state hazards as any other static field, so the same fixes apply:
class ThreadSafeSettings {
private static volatile String paperSize = "A4";
public synchronized void setPaperSize(String size) { // guard the write
paperSize = size;
}
public String getPaperSize() { return paperSize; } // volatile read is safe unguarded
}5. Monostate's Advantage: Inheritance and Polymorphism
Because Monostate uses ordinary instantiation, it composes cleanly with subclassing — something Singleton actively resists:
class PrinterSettings {
protected static String paperSize = "A4";
public void setPaperSize(String size) { paperSize = size; }
public String getPaperSize() { return paperSize; }
}
// A subclass adds behavior while transparently sharing the same static state —
// this is awkward-to-impossible with a classic Singleton, since Singleton's
// "one instance" guarantee is usually tied to a specific concrete class.
class EcoPrinterSettings extends PrinterSettings {
@Override
public void setPaperSize(String size) {
System.out.println("Eco mode: defaulting to recycled stock for " + size);
super.setPaperSize(size);
}
}
PrinterSettings normal = new PrinterSettings();
EcoPrinterSettings eco = new EcoPrinterSettings();
eco.setPaperSize("LETTER");
System.out.println(normal.getPaperSize()); // "LETTER" — shared even across subclass instancesEvery PrinterSettings and EcoPrinterSettings instance — regardless of how many are created, in how many places — reads and writes the same static state, and each can still be used fully polymorphically (passed as a PrinterSettings reference, overridden methods, etc.).
6. Common Implementation Pitfalls
Monostate's simplicity hides a couple of sharp edges that are easy to introduce by accident, especially when a class grows over time and new fields are added by someone unfamiliar with the class's shared-state contract.
Pitfall: The "Partial Monostate" Bug
// VIOLATION: a new field was added WITHOUT the static keyword, because the
// author didn't realize (or forgot) that every field in this class must be
// shared, not just the original ones.
class PrinterSettings {
private static String paperSize = "A4"; // shared, as intended
private String orientation = "PORTRAIT"; // NOT static — silently instance-scoped!
public void setPaperSize(String size) { paperSize = size; }
public String getPaperSize() { return paperSize; }
public void setOrientation(String o) { this.orientation = o; } // only affects THIS instance
public String getOrientation() { return orientation; }
}
PrinterSettings a = new PrinterSettings();
PrinterSettings b = new PrinterSettings();
a.setOrientation("LANDSCAPE");
System.out.println(b.getOrientation()); // "PORTRAIT" — inconsistent with paperSize's sharing!This compiles cleanly and produces no warning — orientation silently becomes ordinary per-instance state while paperSize stays shared, and every caller of the class now has to remember which fields are "really" shared and which aren't. There's no compiler enforcement of "every field in a Monostate class must be static"; it's a hand-maintained convention.
This is Monostate's sharpest edge: the class provides zero structural protection against a field silently opting out of the sharing contract. A code reviewer must check every field declaration by eye. Consider a short comment convention (e.g. // MONOSTATE: shared above the field block) or a unit test that asserts two independently constructed instances observe each other's mutations, specifically to catch this regression class.
Pitfall: Assuming Constructor Logic Runs Once
// VIOLATION: initialization logic in the constructor re-runs on EVERY
// instantiation, not once — a common misconception carried over from Singleton.
class PrinterSettings {
private static String paperSize;
public PrinterSettings() {
paperSize = loadDefaultFromConfigFile(); // runs on every `new`, not just the first!
}
}
// Every `new PrinterSettings()` anywhere in the app re-reads the config file
// and resets paperSize back to the default — silently discarding any
// customization a caller made via setPaperSize() earlier.Unlike Singleton (where the constructor runs exactly once, ever), Monostate's constructor runs on every instantiation — because there's nothing restricting instantiation at all. If the constructor has side effects intended to run only once (loading defaults, registering something), a guard is required:
// FIXED: explicit guard against re-running one-time initialization logic
class PrinterSettings {
private static String paperSize;
private static boolean initialized = false;
public PrinterSettings() {
if (!initialized) {
paperSize = loadDefaultFromConfigFile();
initialized = true;
}
}
}7. Monostate vs. a Static Utility Class
A natural question once you've seen Monostate is: "why not just make everything static methods on a utility class and skip instances entirely?" The two look similar but solve different problems.
// Static utility class — no instances at all, no polymorphism, no interfaces
final class PrinterSettingsUtil {
private PrinterSettingsUtil() { } // prevent instantiation entirely
private static String paperSize = "A4";
public static void setPaperSize(String size) { paperSize = size; }
public static String getPaperSize() { return paperSize; }
}
// Usage — no object at all, ever:
PrinterSettingsUtil.setPaperSize("LETTER");The critical difference is that a static utility class cannot implement an interface's instance methods, cannot be passed as a polymorphic reference, and cannot be subclassed to specialize behavior — static methods aren't part of any virtual dispatch table. Monostate keeps ordinary instance methods (so it participates in polymorphism, can implement interfaces, and can be subclassed as shown in Section 5); a static utility class is a flatter, more limited tool.
// Monostate CAN do this — a static utility class cannot:
interface Settings {
void setPaperSize(String size);
String getPaperSize();
}
class PrinterSettings implements Settings { // implements an interface — instances required
private static String paperSize = "A4";
public void setPaperSize(String size) { paperSize = size; }
public String getPaperSize() { return paperSize; }
}
// Now PrinterSettings can be injected anywhere a Settings is expected,
// participate in dependency injection containers, be mocked via a subclass, etc.
// — none of which a static utility class permits.| Static Utility Class | Monostate | |
|---|---|---|
| Instances | None — instantiation is typically blocked | Ordinary instances, freely created |
| Implements interfaces | No — static methods can't satisfy an interface contract | Yes — instance methods can |
| Subclassing / specialization | Not possible | Fully supported (Section 5) |
| Passable as a polymorphic reference / injectable | No | Yes |
| When it's the better choice | Pure stateless helper functions (Math, Collections) with no need for polymorphism | Shared mutable state that still needs to look and behave like an ordinary, potentially-interface-implementing object |
Rule of thumb: if the shared thing is genuinely stateless (pure functions), use a static utility class — it's simpler and more honest about having no instance identity at all. Reach for Monostate specifically when you need shared mutable state dressed up as an ordinary, polymorphism-compatible object — for instance, because some framework or interface requires an instance, not a static method reference.
8. Testing Monostate Classes
Shared static state creates exactly the same test-pollution risk in Monostate as it does in Singleton or any other static field — tests that mutate the shared state can leak into other tests unless carefully isolated:
class PrinterSettingsTest {
@Test
void testLetterSize() {
PrinterSettings settings = new PrinterSettings();
settings.setPaperSize("LETTER");
assertEquals("LETTER", settings.getPaperSize()); // passes in isolation
}
@Test
void testDefaultSize() {
PrinterSettings settings = new PrinterSettings(); // a BRAND NEW instance...
assertEquals("A4", settings.getPaperSize());
// ...but FAILS if testLetterSize() ran first in the same JVM, because
// the static field was never reset — "new" doesn't mean "fresh state."
}
}This is the same class of flakiness problem covered for Singleton in the Singleton Pattern guide — the fix is the same shape too: an explicit reset hook, run in @BeforeEach/@AfterEach, or isolating each test in its own process/classloader:
class PrinterSettings {
private static String paperSize = "A4";
// ...
// Package-private or test-only reset hook — an explicit admission that
// static state needs explicit lifecycle management in tests.
static void resetForTesting() { paperSize = "A4"; }
}
class PrinterSettingsTest {
@BeforeEach
void resetSharedState() {
PrinterSettings.resetForTesting();
}
// ... tests are now order-independent
}Needing a resetForTesting() escape hatch at all is a signal, not just a workaround — it's direct evidence that this class carries state that isn't scoped to a request/test/unit of work, and that every test touching it is implicitly coupled to every other test's execution order unless this reset discipline is followed consistently. Weigh this cost honestly against Monostate's call-site convenience before adopting it in new code.
Monostate Inside a DI Container
A subtler pitfall shows up when Monostate classes are registered as beans in a dependency-injection container (Spring, etc.):
@Component
class PrinterSettings {
private static String paperSize = "A4"; // still static — still shared across ALL instances
public void setPaperSize(String size) { paperSize = size; }
public String getPaperSize() { return paperSize; }
}Spring's default bean scope already makes PrinterSettings a de facto singleton bean — exactly one instance is created and injected everywhere. Adding static fields on top of that doesn't add sharing (the container already guarantees one instance); it just reintroduces the same hidden-static-state risk described in this guide's pitfalls, now hidden underneath a framework that was already providing the sharing safely via ordinary instance fields.
If you're already inside a DI container that manages bean scope, Monostate's static-field mechanism is almost always redundant and actively harmful — it silently shares state even if someone later registers the bean as @Scope("prototype") expecting independent instances. Inside a DI-managed codebase, prefer ordinary instance fields on a singleton-scoped bean; reserve Monostate for the specific case described in Section 1 — legacy code with scattered new X() calls that a container doesn't manage at all.
9. Monostate vs. Singleton
This is the comparison interviewers most often ask for directly, since the two patterns are frequently confused:
| Singleton | Monostate | |
|---|---|---|
| Object identity | Exactly one object; a == b is always true for any two references | Many distinct objects; a == b is false unless it's literally the same reference |
| How sharing is enforced | Private constructor + static accessor prevents a second object from existing at all | Public, ordinary constructor; sharing is achieved via static fields, not instance restriction |
| Call-site impact of adopting it | Every new X() call site must change to X.getInstance() | Zero call-site changes — existing new X() code keeps working and becomes shared automatically |
| Subclassing | Difficult — usually requires a final class or careful protected-constructor gymnastics | Natural — subclasses inherit and share static state transparently, full polymorphism available |
| Discoverability | Explicit — getInstance() signals "this is special" to every reader | Implicit — looks like an ordinary class; a reader has to notice the static fields to realize state is shared |
| Retrofitting onto legacy code | Invasive (constructor becomes private, call sites break) | Non-invasive (drop-in — existing instantiation code is unaffected) |
Monostate's biggest practical risk is exactly its biggest convenience: because construction looks completely ordinary, a reader (or a new team member) has no signal from the call site that state is shared globally. A new PrinterSettings() call reads like it creates independent state — it doesn't. This hidden-ness is worse for long-term maintainability than Singleton's getInstance(), which at least announces itself. Document Monostate classes clearly, and prefer Singleton (or DI-managed shared beans) when the sharing should be obvious to readers.
10. When to Use vs. When It's Overkill
| Use Monostate when | Avoid it when |
|---|---|
You need shared-state semantics retrofitted onto a codebase with many existing new X() call sites you can't easily change | You're writing new code — Singleton or DI-managed shared beans are more discoverable and equally effective |
| Subtypes of the shared-state class need to add specialized behavior while still sharing the base state | The class doesn't actually need subclassing — the flexibility is unused complexity |
You want value-object-like usage syntax (new X(), pass around freely) but with process-wide shared configuration underneath | Multiple independent configurations should coexist (e.g. per-tenant, per-request settings) — shared static state breaks that requirement entirely |
| Interop with frameworks/libraries that instantiate your class themselves and won't call a static factory method | You need the sharing to be obvious at every call site for code review and onboarding clarity |
11. Real-World / Production Examples
- Legacy codebase migrations. Introducing process-wide shared configuration into a large, procedural-turned-OO codebase with hundreds of scattered
new SomeConfig()call sites — Monostate is frequently the only migration path that doesn't require touching every call site in a single risky change. - Python's Borg idiom (the pattern's origin, via shared
__dict__rather than static fields — Java'sstaticfields are the direct structural equivalent): widely used in scripting and plugin systems where instantiation is driven by a host framework, not by your own code, so you can't force callers through a factory method. - Test fixture builders. A
TestContextinstantiated freely in every test method but sharing ID/sequence generators across a run via static counters, so unique identifiers stay unique across the whole suite without threading a shared context object through every helper method by hand. - Embedded and plugin architectures. Frameworks that instantiate your class themselves via reflection or a service-provider interface (e.g.
ServiceLoader) won't call a staticgetInstance()factory method — they callnewdirectly. If shared state is required under that constraint, Monostate is frequently the only pattern that fits, since Singleton's private-constructor requirement is incompatible with framework-driven instantiation. - Feature-flag / experiment-state shims. A thin
FeatureFlagsclass instantiated per-call-site for readability (new FeatureFlags().isEnabled("x")reads better inline than a long static import) while the underlying flag values are fetched once and cached in shared static state. - Counter and metrics accumulator objects handed out to many unrelated call sites that each construct their own local reference but need to contribute to one running total — Monostate avoids threading a shared accumulator object through every method signature.
Interview Questions
- What is the fundamental mechanism difference between Singleton and Monostate — one sentence each?
- Why would a team choose Monostate over Singleton when retrofitting shared state onto an existing codebase?
- Why does Monostate support inheritance and polymorphism more naturally than Singleton?
- What's the biggest risk of Monostate's "looks like an ordinary class" property, and how would you mitigate it in code review?
- Is Monostate thread-safe by default? What would you add to make it safe under concurrent access?
- What is the "partial Monostate" bug, and why does the compiler not catch it for you?
- Why does constructor logic in a Monostate class need an explicit "run once" guard, unlike Singleton's constructor?
- If two
PrinterSettingsinstances are compared with==, what's the result under Monostate, and why does that surprise developers coming from Singleton? - Why can't a static utility class (all-static methods, no instances) implement an interface the way a Monostate class can? Why does that matter?
- How would you write a JUnit test suite for a Monostate class so that tests don't leak state into each other?
- In what scenario would neither Singleton nor Monostate be appropriate, and dependency injection with per-request scope should be used instead?