Skip to content

Factory, Builder and the Singleton trap

Three creation patterns on small Python code, and the cases where each one is the wrong choice, especially the Singleton, which hides shared state and makes tests fail.

  • Lesson 108 of 125
  • 12 min read
  • Depth
  • Free to read, no account
On this page
  1. Why interviewers ask this
  2. The idea in plain words
  3. Pause and predict
  4. Worked example
  5. Trade-offs
  6. Common misconceptions
  7. How to say it in the interview
  8. Check your understanding
  9. Answers
  10. Sources

What you need first

This lesson uses ideas from these earlier lessons. Read them first if any idea is new to you.

01Why interviewers ask this

The interviewer in a machine-coding round may ask: "Why add a class only to create objects?" or "Should this class have only one object?"

A weak answer uses a pattern name because it sounds good. A strong answer says what problem the pattern solves here, and what it costs. At SDE-3 level you should say when not to use it.

02The idea in plain words

Think about a juice shop. You say "mango", and the shop makes a mango juice. That is a factory. A thali is a plate with many small dishes. The cook adds what you choose and checks the plate before it leaves. That is a builder. One shared water tap that every worker can change is the Singleton trap.

A design pattern is a name for a code shape that solves a problem that comes again and again. A smell is a sign in the code that a pattern may help. These patterns create objects. We use the food order of lesson 106.

Factory: one place that chooses the class

A factory is a function that creates the right object for a name. The smell is a chain of if kind == "upi" checks, repeated wherever the name arrives. A dictionary is enough:

python
class UpiPayment:
    def pay(self, amount):
        return f"UPI paid Rs {amount}"

# CardPayment and CashOnDelivery have the same shape

PAYMENTS = {"upi": UpiPayment, "card": CardPayment, "cod": CashOnDelivery}

def make_payment(kind):
    if kind not in PAYMENTS:
        raise ValueError(f"unknown payment kind: {kind}")
    return PAYMENTS[kind]()

A new kind is a new class and one new line in the dictionary. This is the open-closed rule of lesson 106.

When not to use it. With one class, or when the caller already knows the class, write UpiPayment(). It is clearer.

Builder: build a complex object step by step

An order may have many optional parts, and a call such as Order(items, "98000", None, None, "", False) is hard to read. A builder collects the parts one at a time. Then build() checks them.

python
class Order:
    def __init__(self, items, phone, coupon="", note=""):
        self.items, self.phone = items, phone
        self.coupon, self.note = coupon, note

    def subtotal(self):
        return sum(price * qty for price, qty in self.items)

class OrderBuilder:
    def __init__(self):
        self._items, self._phone = [], ""
        self._coupon, self._note = "", ""

    def item(self, price, qty):
        self._items.append((price, qty))
        return self

    def coupon(self, code):
        self._coupon = code
        return self

    def note(self, text):
        self._note = text
        return self

    def phone(self, number):
        self._phone = number
        return self

    def build(self):
        if not self._items:
            raise ValueError("an order needs at least one item")
        if not self._phone:
            raise ValueError("an order needs a phone number")
        return Order(tuple(self._items), self._phone, self._coupon, self._note)

Each method returns the builder, so the calls chain. No half-made order exists.

When not to use it. With three or four values, use keyword arguments (arguments with names), such as Order(items=x, phone="98000").

Singleton: one object, global access

A Singleton is a class that allows only one object, and any code can reach it. People use it for a counter or settings. Here is one for order IDs:

python
class OrderIds:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.last = 0
        return cls._instance
    def next(self):
        self.last += 1
        return self.last

OrderIds() always returns the same object. That is the trap:

  1. Hidden shared state. A test that places an order changes the counter for every later test.
  2. Hidden dependency. A method that calls OrderIds() does not show that it needs IDs, and you cannot pass in a fake (lesson 106).
  3. Anyone can change it.

The fix is to separate "we need one" from "the class forces one". Write a normal class, create one object at start-up, and pass it in through the constructor (dependency injection, lesson 106). The program has one, and a test makes its own.

python
class IdSource:
    def __init__(self):
        self.last = 0
    def next(self):
        self.last += 1
        return self.last

class OrderService:  # a small service: it gets the source from outside
    def __init__(self, ids):
        self.ids = ids
    def place(self):
        return self.ids.next()

Stuck on something? The AI tutor has read this lesson. Ask it with a free account

03Pause and predict

Pause. Guess first.

Test A places two orders and expects IDs 1 and 2. Test B then places one order and expects ID 1. Both use the OrderIds Singleton in the same program. What ID does test B see?

Write your guess before you read on. The answer is in the worked example.

04Worked example

The order is 2 biryani at Rs 250 and 1 lassi at Rs 60. The subtotal is 2 x 250 + 60 = Rs 560 (lesson 106).

Factory. make_payment("upi").pay(560) returns UPI paid Rs 560. For wallets, we write WalletPayment and add one dictionary line. Changed lines in old code: 0.

Builder. OrderBuilder().item(250, 2).item(60, 1).phone("98000").build() gives an order with subtotal 560. With no items, build() raises an order needs at least one item.

Singleton. Three tests run in one program, in this order: A places 2 orders, B places 1, and C places 3. Each expects its first ID to be 1. With OrderIds:

A sees 1 and 2. B sees 3, which answers the predict box, and B fails. C sees 4, 5 and 6, and C fails. With IdSource, each test builds OrderService(IdSource()). A sees 1 and 2, B sees 1, and C sees 1, 2 and 3. All pass, in any order.

05Trade-offs

  • A factory solves one place that chooses the class. It costs one more step to read. I would skip it for one kind.
  • A builder solves unreadable constructors and half-made objects. It costs an extra class. I would use keyword arguments for few values.
  • A Singleton solves a guarantee of one object. It costs hidden shared state and failing tests. I would create one object and pass it in.

06Common misconceptions

"A pattern makes the code better, so I should use all three." Each adds code. Use one when its smell appears.

"A Singleton is how I make sure there is only one." Passing one object to everyone gives you one, with no global access.

07How to say it in the interview

Say it like this

I use a factory when a name chooses the class, and a builder when an object has many optional parts to check. I avoid a Singleton, because it hides shared state, so I create one object and pass it in.

08Check your understanding

  1. Name one smell for Factory and one for Builder. Why is a Singleton a trap?
  2. Tests A, B and C run in this order and place 3, 2 and 4 orders. Each expects its first ID to be 1. With the OrderIds Singleton, which IDs does each test see, and which tests fail?
  3. A teammate writes a Singleton Database. The tests change each other's data. What do you do?

09Answers

Show the answers
  1. Factory: the same if kind == ... chain in many places. Builder: a constructor with many optional values. A Singleton hides shared state, so tests change each other.
  2. A sees 1, 2 and 3. B sees 4 and 5. C sees 6, 7, 8 and 9. B and C fail.
  3. Make Database a normal class. Create one object at start-up and pass it in. A test passes a fresh in-memory copy, so tests no longer share data.

10Sources

Try it now, no sign-up

Ask the AI tutor about this lesson

The tutor has read this lesson. Ask in your own words, any time. Free account, 20 questions a day.

Reading never needs an account. The account is for the tutor, the quiz, your notes and your progress.

All lessons in Module 17: Low-level design and machine coding