InterviewKata
Design patterns · 05 of 26 · Making objects

Singleton, and why to inject instead

A singleton promises two things: exactly one instance, and access to it from anywhere. This page builds Pedal's pricing config as a singleton, watches it leak state between tests, then keeps the first promise and drops the second by creating one config at startup and passing it in.

1. The pain

Pedal's prices live in one configuration object: price per minute, members' free minutes, peak windows. Checkout, receipts and the monthly report all need the same values, and it would be wrong for two copies to disagree. "There must be exactly one" sounds like a singleton.

So the config became a singleton: PricingConfig.instance(), callable from anywhere. Then the test suite started failing at random. One test changed the price per minute to check a promotion; every test that ran after it in the same process saw the changed price. Nothing in price_member_ride(minutes)'s signature says it reads a global, so the cause took a day to find.

2. The idea in one picture

An office has one wall clock everyone reads. That works until someone moves it forward to test the fire alarm and forgets; now every meeting in the building starts late, and nobody knows why. Compare a watch handed to each person by reception: they can still all be set from the same master clock, but changing one doesn't move the others, and you can see who has which.

Singleton makes a class responsible for having only one instance and for providing a global way to reach it: a private constructor plus a static instance() method. It solves two problems at once: "only one" and "reachable from anywhere". The first is often a real need; the second is the source of most of the trouble.

The usual fix keeps the first and drops the second: create the one instance at startup (the composition root) and pass it to the objects that need it through their constructors. The running app still has exactly one config; tests make their own. This is dependency injection, covered fully on page 24.

3. The roles

RoleIts jobIn Pedal
SingletonHolds its single instance and hides its constructor.PricingConfig with instance()
ClientReaches the instance through the static method.price_member_ride()
Composition root (the alternative)Creates the one instance at startup and passes it on.main() building Pricer(shared) and ReceiptWriter(shared)
Injected client (the alternative)Receives the instance in its constructor.Pricer(config)

4. Before and after

The same "one config" requirement, two ways (Python sketch; the full program is in section 5).

Without the pattern

class PricingConfig:
    _instance = None

    @classmethod
    def instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

def price_member_ride(minutes):
    cfg = PricingConfig.instance()
    ...

With it

class Pricer:
    def __init__(self, config):
        self.config = config

    def member_ride(self, minutes):
        ...

# at startup, once:
shared = Config()
checkout = Pricer(shared)
receipts = ReceiptWriter(shared)
The next changeWithout the patternWith it
A test needs 12p a minuteChange the global, and hope every later test resets itPricer(Config(per_minute=12)), private to that test
Paris needs different prices in the same processImpossible: there is only one instancePass Paris's config to Paris's pricer
Find what depends on the configSearch for instance() calls in bodiesRead the constructors

Injection keeps "one instance in production" and drops "reachable from anywhere", which is the part that hurts.

5. The code

A lazily created singleton read by price_member_ride(), two tests that interfere through it, and the injected version where each test owns its config while the app still shares one.

from __future__ import annotations
from dataclasses import dataclass


# ---- The classic singleton: one instance, reachable from anywhere. ----
class PricingConfig:
    _instance: PricingConfig | None = None

    def __init__(self) -> None:
        self.per_minute = 10
        self.free_minutes = 30

    @classmethod
    def instance(cls) -> PricingConfig:
        if cls._instance is None:      # created on first use
            cls._instance = cls()
        return cls._instance


def price_member_ride(minutes: int) -> int:
    cfg = PricingConfig.instance()     # a dependency you can't see from the signature
    return max(0, minutes - cfg.free_minutes) * cfg.per_minute


# ---- The alternative: still one config in the running app, but handed to whoever needs it. ----
@dataclass
class Config:
    per_minute: int = 10
    free_minutes: int = 30


class Pricer:
    def __init__(self, config: Config) -> None:
        self.config = config           # the dependency is in the constructor

    def member_ride(self, minutes: int) -> int:
        return max(0, minutes - self.config.free_minutes) * self.config.per_minute


class ReceiptWriter:
    def __init__(self, config: Config) -> None:
        self.config = config


if __name__ == "__main__":
    print(f"singleton: same object every time -> {PricingConfig.instance() is PricingConfig.instance()}")

    # Two tests that share the process, run one after the other.
    PricingConfig.instance().per_minute = 12                     # test A changes the price
    print(f"test A sets 12p a minute: 42-minute ride costs {price_member_ride(42)}p")
    got = price_member_ride(42)                                  # test B expects the default
    print(f"test B expects 120p, gets {got}p -> {'pass' if got == 120 else 'FAIL (test A leaked)'}")

    a, b = Pricer(Config(per_minute=12)).member_ride(42), Pricer(Config()).member_ride(42)
    print(f"injected: test A gets {a}p, test B gets {b}p -> {'both pass' if (a, b) == (144, 120) else 'FAIL'}")

    shared = Config()                                            # the composition root makes one
    checkout, receipts = Pricer(shared), ReceiptWriter(shared)
    print(f"app wiring: checkout and receipts share one config -> {checkout.config is receipts.config}")
// ---- The classic singleton: one instance, reachable from anywhere. ----
class PricingConfig {
  private static only: PricingConfig | undefined;
  perMinute = 10;
  freeMinutes = 30;

  private constructor() {}

  static instance(): PricingConfig {
    if (!PricingConfig.only) PricingConfig.only = new PricingConfig();   // created on first use
    return PricingConfig.only;
  }
}

function priceMemberRide(minutes: number): number {
  const cfg = PricingConfig.instance();   // a dependency you can't see from the signature
  return Math.max(0, minutes - cfg.freeMinutes) * cfg.perMinute;
}

// ---- The alternative: still one config in the running app, but handed to whoever needs it. ----
class Config {
  constructor(public perMinute = 10, public freeMinutes = 30) {}
}

class Pricer {
  constructor(readonly config: Config) {}   // the dependency is in the constructor

  memberRide(minutes: number): number {
    return Math.max(0, minutes - this.config.freeMinutes) * this.config.perMinute;
  }
}

class ReceiptWriter {
  constructor(readonly config: Config) {}
}

console.log(`singleton: same object every time -> ${PricingConfig.instance() === PricingConfig.instance() ? "True" : "False"}`);

// Two tests that share the process, run one after the other.
PricingConfig.instance().perMinute = 12;                                  // test A changes the price
console.log(`test A sets 12p a minute: 42-minute ride costs ${priceMemberRide(42)}p`);
const got = priceMemberRide(42);                                          // test B expects the default
console.log(`test B expects 120p, gets ${got}p -> ${got === 120 ? "pass" : "FAIL (test A leaked)"}`);

const a = new Pricer(new Config(12)).memberRide(42), b = new Pricer(new Config()).memberRide(42);
console.log(`injected: test A gets ${a}p, test B gets ${b}p -> ${a === 144 && b === 120 ? "both pass" : "FAIL"}`);

const shared = new Config();                                              // the composition root makes one
const checkout = new Pricer(shared), receipts = new ReceiptWriter(shared);
console.log(`app wiring: checkout and receipts share one config -> ${checkout.config === receipts.config ? "True" : "False"}`);
public class Main {
    // ---- The classic singleton: one instance, reachable from anywhere. ----
    static final class PricingConfig {
        int perMinute = 10;
        int freeMinutes = 30;

        private PricingConfig() {}

        // The holder idiom: created on first use, and thread-safe because class loading is.
        private static final class Holder {
            static final PricingConfig INSTANCE = new PricingConfig();
        }

        static PricingConfig instance() { return Holder.INSTANCE; }
    }

    static int priceMemberRide(int minutes) {
        PricingConfig cfg = PricingConfig.instance();   // a dependency you can't see from the signature
        return Math.max(0, minutes - cfg.freeMinutes) * cfg.perMinute;
    }

    // ---- The alternative: still one config in the running app, but handed to whoever needs it. ----
    static final class Config {
        int perMinute, freeMinutes;
        Config() { this(10, 30); }
        Config(int perMinute, int freeMinutes) { this.perMinute = perMinute; this.freeMinutes = freeMinutes; }
    }

    static final class Pricer {
        final Config config;
        Pricer(Config config) { this.config = config; }   // the dependency is in the constructor

        int memberRide(int minutes) { return Math.max(0, minutes - config.freeMinutes) * config.perMinute; }
    }

    static final class ReceiptWriter {
        final Config config;
        ReceiptWriter(Config config) { this.config = config; }
    }

    static String py(boolean b) { return b ? "True" : "False"; }

    public static void main(String[] args) {
        System.out.println("singleton: same object every time -> " + py(PricingConfig.instance() == PricingConfig.instance()));

        // Two tests that share the process, run one after the other.
        PricingConfig.instance().perMinute = 12;                                   // test A changes the price
        System.out.println("test A sets 12p a minute: 42-minute ride costs " + priceMemberRide(42) + "p");
        int got = priceMemberRide(42);                                             // test B expects the default
        System.out.println("test B expects 120p, gets " + got + "p -> " + (got == 120 ? "pass" : "FAIL (test A leaked)"));

        int a = new Pricer(new Config(12, 30)).memberRide(42), b = new Pricer(new Config()).memberRide(42);
        System.out.println("injected: test A gets " + a + "p, test B gets " + b + "p -> " + (a == 144 && b == 120 ? "both pass" : "FAIL"));

        Config shared = new Config();                                              // the composition root makes one
        Pricer checkout = new Pricer(shared);
        ReceiptWriter receipts = new ReceiptWriter(shared);
        System.out.println("app wiring: checkout and receipts share one config -> " + py(checkout.config == receipts.config));
    }
}

Output (the same in all three languages)

singleton: same object every time -> True
test A sets 12p a minute: 42-minute ride costs 144p
test B expects 120p, gets 144p -> FAIL (test A leaked)
injected: test A gets 144p, test B gets 120p -> both pass
app wiring: checkout and receipts share one config -> True

6. Try it

Run the test suite

Four tests share one process. Two of them change the config. Run them in different orders, with a singleton and with injection.

    A: sets 12p a minute, expects a 42-minute member ride to cost 144p. B: expects the default, 120p. C: sets 45 free minutes, expects 0p. D: expects a 50-minute ride to cost 200p.

    7. When not to use it

    What you get

    • Exactly one instance: when the domain truly needs it (one connection pool, one hardware handle).
    • Lazy creation: built on first use.
    • No wiring: any code can reach it without being passed it.
    • Familiar: everyone recognises the shape.

    What you pay

    • Hidden dependencies: signatures don't show what a function reads.
    • Global mutable state: one change is seen everywhere, including other tests.
    • Hard to test: you can't easily give one test a different instance.
    • Concurrency traps: lazy creation must be made thread-safe; shared state must be too.
    If…Use instead
    You need one instance in the running appCreate it once at startup and inject it: 24 Dependency injection
    The values never change after startupAn immutable config object, passed in
    You need one instance per city, tenant or requestA registry or scoped objects, not a singleton
    You want a few shared, swappable servicesA DI container's "singleton scope": one instance, still injected

    8. How it goes wrong

    A singleton as a global variableIf the reason is "it's convenient to reach from anywhere", that's a global with extra steps. Pass it in.
    Mutable state in the singletonEvery write is seen by every reader, including other tests and threads. Keep singletons immutable, or don't have them.
    Unsafe lazy creationTwo threads can both see None and create two instances. Use the language's safe idiom (Java's holder class or enum), or create it eagerly.
    Singletons that reach other singletonsChains of instance() calls hide a whole dependency graph that only shows up at runtime.

    9. Remember it and say it

    Memory hook: "One clock on the wall"One shared instance everyone reads, and anyone can move.
    How this hook works: what “One clock on the wall” means

    What the slogan means, phrase by phrase

    “One clock”
    The single instance.
    “on the wall”
    Reachable from anywhere via instance().
    “anyone can move”
    Global mutable state: one change is everyone's change.

    Why this picture fits: A shared wall clock shows both the convenience and the danger: one source, but one mistake moves everyone.

    The story → the concept

    The officeThe code
    The wall clockPricingConfig.instance()
    Moving it to test the alarmTest A setting 12p a minute
    Everyone late afterwardsTest B getting 144p
    A watch from receptionAn injected Config

    Watch it on an example

    1. Test A sets the singleton's price to 12p a minute.
    2. Test B runs next in the same process and expects the default.
    3. It gets 144p instead of 120p, because both tests read the same object.

    The trap it saves you from

    Believing "only one" requires "reachable from anywhere".

    It still works when…

    Yes: logging, metrics and connection pools are the classic acceptable singletons, because they're shared and rarely change behaviour. Even those are easier to test when injected.

    In an interview or code review

    10. Check yourself

    Which two things does a classic singleton promise?

    The second is the one that causes most problems.

    Test A changes the singleton; test B then fails. Why?

    Global mutable state leaks between tests.

    How do you keep "one instance" but drop global access?

    The composition root makes it; constructors receive it.

    What's a thread-safe lazy singleton in Java?

    Class loading guarantees one initialisation.

    Which is the most acceptable use of a singleton?

    Shared, and changing it doesn't change business behaviour.

    11. Go deeper

    ResourceWhy
    24 Dependency injectionThe full alternative: composition roots, constructor injection and containers.
    Low-level design 03e: Dependency inversionWhy depending on what you're given beats reaching out for it.
    Python FAQ: sharing globals across modulesThe module itself is Python's built-in single instance.
    Wikipedia: Singleton patternImplementations and the criticism.