Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design)  Module 4 – Design principles: SOLID and beyond

Law of Demeter, tell-don't-ask and CQS

Three rules for how objects talk: the Law of Demeter, tell-don't-ask and command-query separation, with a chain that breaks and a retry that skips a patient.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Identify chained calls that couple a caller to the structure of distant objects
  • Move behaviour to the object that owns the data, so that callers tell instead of ask
  • Separate commands that change state from queries that return data
  • Explain why a method that both changes state and returns data makes retries unsafe

Before you start

What you’ll learn · Before you start

What you will learn

  • Identify chained calls that couple a caller to the structure of distant objects
  • Move behaviour to the object that owns the data, so that callers tell instead of ask
  • Separate commands that change state from queries that return data
  • Explain why a method that both changes state and returns data makes retries unsafe

Before you start

On this page

Three rules describe how objects should talk to each other. The Law of Demeter says that a method should send messages only to the objects close to it, not to objects it reaches through them. Tell, don’t ask says that instead of reading another object’s data and deciding for it, you tell that object what to do. Command-query separation (CQS) says that a method either changes state or answers a question, never both. All three keep each piece of behaviour next to the data it needs, so that a change in how one object stores something stays inside that object.

The Law of Demeter: talk to your friends

The law began as a style rule in the Demeter project at Northeastern University, where Ian Holland formulated it in 1987. Karl Lieberherr, Ian Holland and Arthur Riel presented it at the OOPSLA conference in 1988, and Lieberherr and Holland described it again in IEEE Software in 1989. The project’s page states it generally:

Each unit should have only limited knowledge about other units: only units “closely” related to the current unit.

Its short form is “only talk to your immediate friends”. For objects, the project spells out who the friends of a method are. A method may send messages to:

  • its parameters, counting the object itself (self) as one;
  • the objects it creates;
  • the object’s immediate parts: its attributes, what its own methods return, and the elements of a collection it holds;
  • for practical reasons, global objects.

Everything reached through a friend is a stranger. In order.customer.wallet.balance, the order is a friend of the code that received it, but the customer is a part of the order, the wallet a part of the customer and the balance a part of the wallet. A caller that writes this chain knows that orders have one customer, that customers have one wallet and that wallets store a number called balance. Each of those facts can change, and each change breaks the caller.

A chain that breaks when the structure changes

A college canteen’s till takes payment for an order. The first version of the model gives every customer one wallet. Two tills use it. pay_by_asking() reaches through the order to the wallet, compares the balance with the total and subtracts it. pay_by_telling() calls order.pay() and leaves the rest to the objects that own the money. Then the canteen gives every student a meal card as well as a top-up wallet, and a payment uses the meal card first. Both tills, run against both versions of the model:

Two tills, before and after customers got two wallets

chain/main.py

"""Both tills, run against both versions of the model: a 70 order for a customer with enough money."""

import model_v1
import model_v2
from checkout import pay_by_asking, pay_by_telling


def customer(model):
    if model is model_v1:
        return model.Customer("Asha", balance=100)
    return model.Customer("Asha", meal_card=50, top_up=50)


for model in (model_v1, model_v2):
    for till in (pay_by_asking, pay_by_telling):
        order = model.Order(customer(model), prices=[40, 30])
        try:
            result = till(order)
        except AttributeError as error:
            result = f"crashed: {error}"
        print(f"{model.__name__}, {till.__name__}: {result}")

Output

model_v1, pay_by_asking: paid
model_v1, pay_by_telling: paid
model_v2, pay_by_asking: crashed: 'Customer' object has no attribute 'wallet'
model_v2, pay_by_telling: paid

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py

chain/checkout.py

"""Two ways for the till to take payment for an order."""

from errors import InsufficientBalance


def pay_by_asking(order):
    """Reaches through the order and the customer to the wallet, reads its state and decides."""
    wallet = order.customer.wallet
    if wallet.balance < order.total():
        return "declined"
    wallet.balance -= order.total()
    return "paid"


def pay_by_telling(order):
    """Tells the order to pay; the objects that own the money decide how."""
    try:
        order.pay()
    except InsufficientBalance:
        return "declined"
    return "paid"

chain/model_v1.py

"""A college canteen's model, first version: every customer has one wallet."""

from errors import InsufficientBalance


class Wallet:
    def __init__(self, balance):
        self.balance = balance

    def debit(self, amount):
        if amount > self.balance:
            raise InsufficientBalance(f"needs {amount}, has {self.balance}")
        self.balance -= amount


class Customer:
    def __init__(self, name, balance):
        self.name = name
        self.wallet = Wallet(balance)

    def pay(self, amount):
        self.wallet.debit(amount)


class Order:
    def __init__(self, customer, prices):
        self.customer = customer
        self.prices = list(prices)

    def total(self):
        return sum(self.prices)

    def pay(self):
        self.customer.pay(self.total())

chain/model_v2.py

"""Second version: a customer has a meal card and a top-up wallet, and a payment uses the meal card first."""

from errors import InsufficientBalance


class Wallet:
    def __init__(self, balance):
        self.balance = balance

    def debit(self, amount):
        if amount > self.balance:
            raise InsufficientBalance(f"needs {amount}, has {self.balance}")
        self.balance -= amount


class Customer:
    def __init__(self, name, meal_card, top_up):
        self.name = name
        self.wallets = [Wallet(meal_card), Wallet(top_up)]  # the order in which a payment uses them

    def pay(self, amount):
        if amount > sum(wallet.balance for wallet in self.wallets):
            raise InsufficientBalance(f"needs {amount}")
        for wallet in self.wallets:
            part = min(amount, wallet.balance)
            wallet.debit(part)
            amount -= part


class Order:
    def __init__(self, customer, prices):
        self.customer = customer
        self.prices = list(prices)

    def total(self):
        return sum(self.prices)

    def pay(self):
        self.customer.pay(self.total())

chain/errors.py

"""The canteen's payment error, shared by both versions of the model and by the till."""


class InsufficientBalance(Exception):
    pass

The asking till crashes on the new model, because customer.wallet no longer exists. Its failure would have been worse if the model had kept a wallet attribute for the top-up wallet: the till would have quietly ignored the meal card, because the rule “meal card first” lives in Customer.pay() and the asking till has its own copy of an older rule. The telling till needed no change at all. The change to how customers pay was made in one method, by the class that knows how customers hold money.

Asking, the till talks to the order, the customer and the wallet. Telling, the till talks to the order, which talks to its customer.1. Asking: the till reaches three objects2. Telling: each object talks to its own partpay_by_asking(order)OrderCustomerWalletpay_by_telling(order)OrderCustomerWallettotal(), .customer.walletreads andchanges .balancepay()pay(total)debit(amount)

Who talks to whom when the till takes a payment

Text description of the diagram

The diagram shows the same payment in two designs, one above the other. Each arrow points from the code that sends a message to the object that receives it.

  1. Asking: pay_by_asking(order) calls total() on the order and reads its customer, reads the customer's wallet, then reads and changes the wallet's balance itself. The till depends on three classes and on how each one stores its data.
  2. Telling: pay_by_telling(order) calls pay() on the order. The order calls pay(total) on its customer, and the customer calls debit(amount) on its wallet. Each object sends messages only to an object it holds, and the till depends on the order alone.

Tests show the same difference. A test of the asking till needs a fake order that returns a fake customer that returns a fake wallet with a balance, three fakes nested inside each other. A test of the telling till needs one fake order with a pay() method.

Common mistake

Hiding a chain behind getters, as in order.get_customer().get_wallet().get_balance(), keeps every dependency of the chain. So does adding a forwarding method for every inner attribute, such as order.customer_wallet_balance(), which only moves the structure into the order’s interface. Move the behaviour that needs the data, here paying, to the object that holds the data.

Tell, don’t ask

The asking till shows the habit that tell-don’t-ask names. It asks the wallet for its balance, decides on the wallet’s behalf and then changes the wallet’s state. Martin Fowler credits the phrase to Andy Hunt and Dave Thomas, and describes it as a reminder of what object orientation is for: data and the behaviour that works on it belong together, so instead of pulling data out of an object and deciding outside it, you tell the object what you want done.

Fowler also warns against reading it as a ban on queries. He writes that he does not use tell-don’t-ask as a rule himself, but tries to put data and behaviour together, which leads to much the same place. He has seen it turn people into “GetterEradicators” who remove every query method, although asking an object for information is often the clearest design. A useful reading: when a caller reads an object’s state in order to change that same object, move the decision into the object. Reading data to show it, print it or pass it on is fine.

Command-query separation

Bertrand Meyer, in Object-Oriented Software Construction, divides methods into two kinds. A command changes the state of an object and returns nothing. A query returns a value and changes nothing that callers can observe. Fowler adds a practical exception: popping a stack both changes it and returns the top element, and he keeps that idiom while following the rule wherever he can. The benefit of the split is that queries are safe: they can be called twice, in any order, from a test or a log line, and the answer does not change because someone asked.

The cost of mixing the two shows up when something fails halfway. A clinic’s waiting room calls patients by token number. The first queue has one method, next_and_announce(), that removes the next token and shows it on the display. The second has a query, peek(), and a command, remove(token). The display fails on its first call, and each caller retries once:

A retry with a mixed method, and with a query and a command

cqs/retry_check.py

"""The waiting-room display fails once and works the second time. The caller retries once in both designs."""

from token_queues import MixedQueue, SeparatedQueue


class FlakyDisplay:
    def __init__(self):
        self.calls = 0
        self.shown = []

    def announce(self, token):
        self.calls += 1
        if self.calls == 1:
            raise ConnectionError("display did not answer")
        self.shown.append(token)


def call_next_mixed(queue, display):
    for attempt in (1, 2):
        try:
            return queue.next_and_announce(display)
        except ConnectionError:
            continue


def call_next_separated(queue, display):
    for attempt in (1, 2):
        token = queue.peek()
        try:
            display.announce(token)
        except ConnectionError:
            continue
        queue.remove(token)
        return token


for name, queue, call_next in (
    ("mixed", MixedQueue(["T-1", "T-2", "T-3"]), call_next_mixed),
    ("separated", SeparatedQueue(["T-1", "T-2", "T-3"]), call_next_separated),
):
    display = FlakyDisplay()
    call_next(queue, display)
    print(f"{name:10} announced {display.shown}, still waiting {queue.waiting()}")

Output

mixed      announced ['T-2'], still waiting ['T-3']
separated  announced ['T-1'], still waiting ['T-2', 'T-3']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 retry_check.py

cqs/token_queues.py

"""A clinic's token queue in two designs: one method that removes and announces, or a query and a command."""

from collections import deque


class MixedQueue:
    def __init__(self, tokens):
        self._tokens = deque(tokens)

    def next_and_announce(self, display):
        """Removes the next token AND announces it: a command and a query in one method."""
        token = self._tokens.popleft()
        display.announce(token)
        return token

    def waiting(self):
        return list(self._tokens)


class SeparatedQueue:
    def __init__(self, tokens):
        self._tokens = deque(tokens)

    def peek(self):
        """A query: which token is next? It changes nothing, so it is safe to ask again."""
        return self._tokens[0]

    def remove(self, token):
        """A command: the token has been called. It returns nothing."""
        if self._tokens[0] != token:
            raise ValueError(f"{token} is not at the front of the queue")
        self._tokens.popleft()

    def waiting(self):
        return list(self._tokens)

With the mixed method, the first attempt removed T-1 before the display failed, so the retry removed and announced T-2, and the patient holding T-1 is never called. With the query and the command, the caller asks which token is next, announces it and removes it only once the announcement worked, so the retry announces T-1. The separated design also makes the order of steps visible in the caller, where the retry policy lives, instead of hiding it inside one method.

What is not a violation

The Law of Demeter is about objects that hide their structure. Code that walks through data whose structure is its public contract is not breaking it, and treating every chain of dots as a fault leads to absurd designs:

  • Fluent builders. Query("orders").where(paid=True).order_by("date") talks to one query object at every step, because each method returns a query.
  • Values and data structures. point.x, config["db"]["host"] or a data transfer object passed between layers expose their fields on purpose, and so does a chain of string or path operations such as name.strip().lower().
  • Your own parts. Inside Room, self.minibar.total() sends a message to an object the room holds, which the law allows.

Forwarding methods have a cost too: every one of them widens the interface of the class that holds it, which the Interface Segregation Principle warns about. The fix for a chain is rarely “add a method that returns the far object”; it is to move the operation, so that the caller asks for what it needs done.

In a design interview

When you write a.b.c.do(), stop and ask which object should own do(). Say it out loud: “the order shouldn’t know how customers store money, so the till calls order.pay() and the customer decides which wallet to use”. For a method that both changes state and returns data, say how a caller retries it safely.

Python Online Compiler Give the second model a third wallet, such as a sports-fee account, and see which till still works.

Interview questions

Warm-up (fresher to mid level): what is the Law of Demeter, and what does a violation look like? It is a style rule from Northeastern University’s Demeter project: a method should send messages only to its friends, which are its own object, its parameters, the objects it creates and the objects its object holds directly. Objects reached through those friends are strangers. A violation is a chain such as order.customer.wallet.balance, where the caller depends on how three other classes store their data, so a change to any of them, such as customers getting two wallets, breaks it. The fix is not a getter for each step but moving the behaviour: the caller tells the order to pay, the order tells its customer, and the customer decides which wallet to use. Fluent builders and plain data structures are not violations.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A checkout function computes cart.customer.membership.discount_rate() and cart.customer.address.region.tax_rate(). The business now lets a company account pay for its employees' orders, with its own discount and billing address. What breaks, and how would you redesign the checkout so that the next change like this touches one class?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A charge_and_get_receipt(order) method charges a card and returns the receipt. When the payment provider times out, the client retries, and some customers are charged twice. How does command-query separation help here, and what else does the design need?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The Law of Demeter: a method talks to itself, its parameters, the objects it creates and the objects it holds, not to objects reached through them.
  • A chain of calls couples the caller to the structure of every object along it, and needs a nested fake for each step in tests.
  • Tell, don’t ask: when a caller reads an object’s state to change that object, move the decision into the object. Queries for showing or passing on data are fine.
  • Command-query separation: commands change state and return nothing; queries return data and change nothing. A method that does both makes retries skip or repeat work.
  • Builders, values and data structures expose their structure on purpose; move operations, not data, to fix a chain.

Exercise

Exercise · Medium · Python

Make three hotel call sites talk only to the booking

A small hotel's code has three call sites that each receive a Booking and then reach through it: invoice_total() adds up booking.room.minibar.items, welcome_line() reads booking.guest.address.city, and add_stay_points() changes booking.guest.loyalty.points itself. Each one depends on how three or four classes store their data.

Move the behaviour to the objects that own the data, so that each call site makes one call on the booking it receives and reads no attribute of an attribute. Inside the classes, an object may still use its own parts, for example a Room using its minibar. The results must stay the same: 7,700 for three nights at 2,500 with 200 from the minibar, the same welcome line, and 10 loyalty points a night. Keep the constructors of Guest, Room and Booking as they are.

The sample tests check the three results and read the source of the three call sites to count chained reads such as booking.guest.name.

Starter code · hotel.py

"""A small hotel's model and three call sites that reach through it. See the exercise prompt for what to change."""


class Address:
    def __init__(self, city):
        self.city = city


class Loyalty:
    def __init__(self, points=0):
        self.points = points


class Guest:
    def __init__(self, name, city, points=0):
        self.name = name
        self.address = Address(city)
        self.loyalty = Loyalty(points)


class Minibar:
    def __init__(self):
        self.items = {}  # item name -> price

    def take(self, item, price):
        self.items[item] = price


class Room:
    def __init__(self, number, rate):
        self.number = number
        self.rate = rate
        self.minibar = Minibar()


class Booking:
    def __init__(self, guest, room, nights):
        self.guest = guest
        self.room = room
        self.nights = nights


# ---- the three call sites --------------------------------------------------------------------------------------


def invoice_total(booking):
    minibar = sum(booking.room.minibar.items.values())
    return booking.room.rate * booking.nights + minibar


def welcome_line(booking):
    return f"Welcome, {booking.guest.name}, from {booking.guest.address.city}"


def add_stay_points(booking):
    booking.guest.loyalty.points = booking.guest.loyalty.points + booking.nights * 10
The sample tests · test_hotel.py
import ast
import inspect

import hotel
from hotel import Booking, Guest, Room, add_stay_points, invoice_total, welcome_line


def make_booking():
    guest = Guest("Meera", "Pune", points=40)
    room = Room(204, rate=2500)
    room.minibar.take("water", 40)
    room.minibar.take("cashews", 160)
    return Booking(guest, room, nights=3)


def chains_in(function):
    """Attribute chains such as a.b.c in a function: an attribute read from the result of another attribute."""
    tree = ast.parse(inspect.getsource(function))
    return [node for node in ast.walk(tree) if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Attribute)]


def test_invoice_total():
    """3 nights at 2500 plus 200 from the minibar"""
    assert invoice_total(make_booking()) == 7700


def test_welcome_line():
    """the guest is greeted by name and home city"""
    assert welcome_line(make_booking()) == "Welcome, Meera, from Pune"


def test_add_stay_points():
    """a stay earns 10 points a night"""
    booking = make_booking()
    add_stay_points(booking)
    assert booking.guest.loyalty.points == 70


def test_call_sites_talk_only_to_the_booking():
    """no call site reads an attribute of an attribute: each one talks only to the booking it receives"""
    for function in (invoice_total, welcome_line, add_stay_points):
        found = chains_in(function)
        assert found == [], f"{function.__name__} still reaches through the booking ({len(found)} chained reads)"


def test_the_model_still_builds_the_same_way():
    """Guest, Room and Booking keep their constructors, so other code that creates them still works"""
    assert isinstance(hotel.Booking(Guest("Ravi", "Kochi"), Room(101, rate=1800), nights=1), Booking)
A hint

Work from the inside out. Minibar can total its own items, Room can say what a stay of some nights costs, and Booking can ask its room for that; then invoice_total() only calls one method of the booking. Do the same for the welcome line (a Guest knows its name and city) and for the points: tell the guest how many points it earned rather than changing its Loyalty from outside.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 Order holds a customer. Inside Order.pay(self, method), which calls keep the Law of Demeter?

    Choose every answer that is right.

    Show the answer to question 1

    Answer:

    • self.total()
    • self.customer.pay(amount), on an object the order holds
    • method.charge(amount), on the parameter
    • Receipt(self).send(), on an object the method has just created

    A method may send messages to itself, to its parameters, to objects it creates and to the objects it holds. The customer's wallet is a part of a part: reaching it ties the order to how customers store their money, so ask the customer to pay instead.

  2. Question 2 of 6 The canteen moved from one wallet per customer to a meal card plus a top-up wallet. What does main.py print?

    What does this program print? Choose one answer.

    """Both tills, run against both versions of the model: a 70 order for a customer with enough money."""
    
    import model_v1
    import model_v2
    from checkout import pay_by_asking, pay_by_telling
    
    
    def customer(model):
        if model is model_v1:
            return model.Customer("Asha", balance=100)
        return model.Customer("Asha", meal_card=50, top_up=50)
    
    
    for model in (model_v1, model_v2):
        for till in (pay_by_asking, pay_by_telling):
            order = model.Order(customer(model), prices=[40, 30])
            try:
                result = till(order)
            except AttributeError as error:
                result = f"crashed: {error}"
            print(f"{model.__name__}, {till.__name__}: {result}")
    Show the answer to question 2

    Answer: it prints

    model_v1, pay_by_asking: paid
    model_v1, pay_by_telling: paid
    model_v2, pay_by_asking: crashed: 'Customer' object has no attribute 'wallet'
    model_v2, pay_by_telling: paid

    The asking till depends on customer.wallet, which the second version replaced with a list of wallets, so it crashes. The telling till only calls order.pay(), and the change to how a customer pays was made inside Customer.pay(), the one place that knows about wallets.

  3. Question 3 of 6 Which change to this caller follows "tell, don't ask"?

    Read the code, then choose one answer.

    if account.balance >= fee:
        account.balance -= fee
        account.history.append(("fee", fee))
    Show the answer to question 3

    Answer: Give Account a charge(fee) method that checks, debits and records, and call it here

    The caller reads the account's state, decides and changes it, so the rule about fees lives outside the object that owns the money and is repeated wherever fees are charged. Telling the account to charge puts the check, the debit and the record together, where they can change in one place.

  4. Question 4 of 6 Which method follows command-query separation?

    Choose one answer.

    Show the answer to question 4

    Answer: peek(), which returns the next token and changes nothing

    Bertrand Meyer's rule splits methods into commands, which change state and return nothing, and queries, which return a value and change nothing you can observe. Only peek() is a pure query; each of the others both changes something and answers a question.

  5. Question 5 of 6 The display fails on its first call and works on the second, and each caller retries once. What does retry_check.py print?

    What does this program print? Choose one answer.

    """The waiting-room display fails once and works the second time. The caller retries once in both designs."""
    
    from token_queues import MixedQueue, SeparatedQueue
    
    
    class FlakyDisplay:
        def __init__(self):
            self.calls = 0
            self.shown = []
    
        def announce(self, token):
            self.calls += 1
            if self.calls == 1:
                raise ConnectionError("display did not answer")
            self.shown.append(token)
    
    
    def call_next_mixed(queue, display):
        for attempt in (1, 2):
            try:
                return queue.next_and_announce(display)
            except ConnectionError:
                continue
    
    
    def call_next_separated(queue, display):
        for attempt in (1, 2):
            token = queue.peek()
            try:
                display.announce(token)
            except ConnectionError:
                continue
            queue.remove(token)
            return token
    
    
    for name, queue, call_next in (
        ("mixed", MixedQueue(["T-1", "T-2", "T-3"]), call_next_mixed),
        ("separated", SeparatedQueue(["T-1", "T-2", "T-3"]), call_next_separated),
    ):
        display = FlakyDisplay()
        call_next(queue, display)
        print(f"{name:10} announced {display.shown}, still waiting {queue.waiting()}")
    Show the answer to question 5

    Answer: it prints

    mixed      announced ['T-2'], still waiting ['T-3']
    separated  announced ['T-1'], still waiting ['T-2', 'T-3']

    The mixed method removed T-1 before the display failed, so the retry removed and announced T-2, and patient T-1 was never called. With a query and a command, the caller asks which token is next, announces it, and removes it only after the announcement worked, so the retry announces T-1.

  6. Question 6 of 6 Which of these chains is not a Law of Demeter problem in practice?

    Choose one answer.

    Show the answer to question 6

    Answer: Query("orders").where(paid=True).order_by("date"), a builder whose methods return a query

    The law is about objects that hide their structure, so that callers do not depend on it. A fluent builder returns a query each time, and the caller only ever talks to that query; the other chains walk through the parts of several objects and break when any of those parts changes.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.