📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the Law of Demeter, and how do you apply it in C# without banning fluent APIs and LINQ?

Principle of least knowledge: talk only to immediate collaborators; train-wreck chains; why fluent builders and LINQ are not violations; refactoring by moving behaviour.

Mid-Level Theory Intermediate coupling design-principles encapsulation law-of-demeter

🎯 Key points

  • A method should only talk to itself, its parameters, objects it creates, and its direct fields
  • Train-wreck chains like order.Customer.Address.City couple the caller to a whole object graph
  • The fix is usually to move behaviour to the object that owns the data
  • Fluent APIs and LINQ return the same kind of object, so they are not violations
  • DTOs and records used as plain data are exempt in practice
  • It reduces coupling and protects encapsulation, at the cost of some wrapper methods

⏱ 30-second answer

The Law of Demeter (principle of least knowledge) says a method should only call methods on: itself, its parameters, objects it creates, and its own fields. A chain like order.Customer.Address.Postcode.StartsWith("6") couples the caller to the structure of four types, so changing any of them breaks it. The fix is usually to ask the nearest object for what you need (order.ShipsToWa()), which moves behaviour to where the data lives. It is about navigating object structures, not counting dots: LINQ and fluent builders return the same abstraction each call, and plain DTOs are data, so neither is a violation.

🗣 Full answer (senior)

How I would answer this in an interview

"The Law of Demeter, or principle of least knowledge, says a method should only talk to its immediate collaborators: the object itself, its parameters, objects it creates, and its own fields. 'Talk to friends, not to strangers.' A chain like order.Customer.BillingAddress.State inside a pricing service couples that service to the internal structure of three other types. When the structure changes (say orders gain a separate delivery address), every caller that navigated the old path breaks, or worse, keeps compiling and silently does the wrong thing. The fix is rarely to add pass-through properties. It's usually to move the decision to the object that owns the data (order.ShipsToRemoteState()), which is Tell, Don't Ask, or to pass only what's needed (a method needing a postcode takes a postcode). It isn't about counting dots: LINQ and fluent builders return the same abstraction at each step, and plain DTOs exist to expose data, so neither is a violation. I use it as a smell detector, especially when tests need deep mocks."


1. The rule

For a method M of object O, M may call members of:

Allowed Example
O itself this.CalculateTotal()
M's parameters discount.Apply(total)
Objects M creates var invoice = new Invoice(...); invoice.Issue();
O's direct fields/properties _repository.SaveAsync(...)
(not) Objects returned by those calls, then their members... order.Customer.BillingAddress.State

2. Violations vs non-violations

Code Violation? Why
order.Customer.BillingAddress.State == "WA" in a freight rule Yes Navigates other objects' internals to make a decision
query.Where(...).OrderBy(...).Select(...) No Each call returns IEnumerable<T>/IQueryable<T> (same abstraction)
builder.Services.AddScoped<..>().AddHttpClient() No Fluent API on the same builder
string.Trim().ToUpperInvariant() No Value transformations
dto.Customer.Email in a mapper Usually fine Plain data, no invariants to protect
db.Orders.Where(o => o.Customer.Address.State == "WA") Fine Translated to SQL; it's a query, not domain logic
_order.GetCustomer().GetAccount().Debit(10) Yes Commands sent through a chain of strangers

3. Why it matters

Without LoD With LoD
Callers depend on the shape of an object graph Callers depend on one collaborator's behaviour
Structural changes ripple to many call sites (or silently break them) Changes stay inside the owner
Rules duplicated wherever the graph is navigated Rule lives once on the owner
Tests need deep mocks (x.A.B.C) Tests fake one collaborator
Invariants bypassed (callers mutate nested objects) Owner enforces invariants

4. Fixing a violation

Situation Fix
Caller needs a decision Move it to the owner: order.ShipsToRemoteState()
Caller needs a value only Pass the value: Surcharge(string deliveryState)
Caller commands a nested object Tell the owner: order.ChangeDeliveryAddress(addr)
Many delegating wrappers appear Responsibility is probably in the wrong class; move it
It's just data (DTO/record) Leave it

Production example

In our [retail] app, [about 30] freight and shipping rules read order.Customer.Address.State. When the business introduced separate delivery addresses, every rule kept compiling and silently used the billing state, so [WA] deliveries to customers billed in other states were undercharged for freight for [weeks]. I added Order.DeliveryState (delivery address if present, otherwise billing) and Order.ShipsToRemoteState(), and changed the rules to ask the order instead of navigating the graph. The fix was one place, the next address change stayed inside Order, and we added a review guideline against service code navigating entity graphs for decisions, plus tests on the order's delivery logic.

🔍 What the interviewer is checking

Checks understanding of encapsulation and coupling at the call-site level.

Green flags

  • States the rule precisely.
  • Explains why LINQ and fluent APIs are not violations.
  • Fixes violations by moving behaviour, not adding wrappers.
  • Mentions deep mocks as a symptom.

Red flags

  • "Never use more than one dot."
  • Unaware of the coupling a chain creates.

📚 Learn in depth

1. Origin

The Law of Demeter came from the Demeter project at Northeastern University (Ian Holland, Karl Lieberherr, 1987). The goal was structure-shy code: an object should know as little as possible about the structure of other objects. The "one dot" rule of thumb is a simplification that causes more confusion than it prevents.

2. Structural vs abstraction chains

Structural (violation):   order → Customer → BillingAddress → State      (3 different types, caller knows the graph)
Abstraction (fine):       IEnumerable<T> → IEnumerable<T> → IEnumerable<T> (same abstraction each step)
Fluent builder (fine):    IServiceCollection → IServiceCollection → ...

The question is "does this caller now depend on how some other object is built internally?"

3. Deep mocks as a smell

// Test needs to fake a whole graph: the unit under test knows too much
var order = Substitute.For<IOrder>();
order.Customer.BillingAddress.State.Returns("WA");

After refactoring:

var order = Substitute.For<IOrder>();
order.ShipsToRemoteState().Returns(true);   // one direct collaborator

Deep mocks are fragile: any structural refactoring breaks them, even when behaviour doesn't change.

4. EF Core and the Law of Demeter

Use Verdict
Navigation in LINQ queries (o.Customer.Address.State inside Where) Fine: it's a query description translated to SQL
Loading a graph with Include and then making decisions in services by walking it Smell: decisions belong on the aggregate
Lazy loading through chains in loops Also a performance problem (N+1 queries)

Aggregates in DDD are the natural place for LoD: outside code talks to the aggregate root, which enforces invariants for its internals.

5. Cost of over-applying it

Over-application Problem
Wrapper for every nested property (order.CustomerBillingAddressState) Bloated classes; same coupling, just hidden
Banning LINQ chains Less readable code with no benefit
Hiding DTO data behind methods Noise

The balance: apply it to objects with behaviour and invariants; relax it for data structures.

6. Refactoring walkthrough

  1. Find chains of 3+ structural hops in service code (grep for \.\w+\.\w+\.\w+ in domain/services folders, then review).
  2. For each, ask: does the caller need a decision, a value, or to command something?
  3. Decision → method on the owner; value → parameter; command → method on the owner.
  4. Update tests to fake only direct collaborators.
  5. Repeat where duplicated rules appear.

7. Try it yourself

  1. Run the code example, then temporarily rewrite Surcharge to use the billing state chain and see the wrong result.
  2. Search a service class for chains of three or more property hops.
  3. Refactor one to a method on the nearest owner and simplify its test.
  4. Find a test with nested mock setups and redesign the code so the test needs one collaborator.
  5. Look for places where callers mutate nested objects (order.Customer.Address.City = ...) and move the command to the owner.

💻 Code

var order = new Order(
    new Customer("Ava", new Address("Perth", "WA", "6000")),
    deliveryAddress: new Address("Darwin", "NT", "0800"));

Console.WriteLine(FreightRules.Surcharge(order));

public sealed record Address(string City, string State, string Postcode);
public sealed record Customer(string Name, Address BillingAddress);

public sealed class Order(Customer customer, Address? deliveryAddress = null)
{
    public Customer Customer { get; } = customer;

    // The order decides where it ships; callers no longer navigate the graph.
    public string DeliveryState => (deliveryAddress ?? Customer.BillingAddress).State;
    public bool ShipsToRemoteState() => DeliveryState is "WA" or "NT";
}

public static class FreightRules
{
    // Violation (do not do this): order.Customer.BillingAddress.State is "WA" or "NT"
    // which silently ignores the delivery address.
    public static decimal Surcharge(Order order) => order.ShipsToRemoteState() ? 25m : 0m;
}

⚠ Common mistakes

1. Counting dots

What happens: LINQ chains and fluent builders flagged as violations. Why it is wrong: they stay on one abstraction; no internals exposed. Do instead: look for navigation through different objects' structure.

2. Pass-through wrappers everywhere

What happens: Order.CustomerBillingAddressState added for each nested field. Why it is wrong: bloated classes with the same coupling. Do instead: move the decision to the owner, or pass only the data needed.

3. Applying it rigidly to DTOs

What happens: DTOs get methods to hide their own data. Why it is wrong: data structures exist to expose data. Do instead: apply LoD to objects with behaviour and invariants.

4. Ignoring deep mocks

What happens: tests set up x.A.B.C chains. Why it is wrong: code under test knows too much; tests break on refactors. Do instead: redesign so the unit needs only direct collaborators.

5. Mutating nested objects from outside

What happens: order.Customer.Address.City = "Perth" in a service. Why it is wrong: bypasses invariants the owner should enforce. Do instead: order.ChangeDeliveryAddress(newAddress).

6. Methods taking whole objects for one value

What happens: CalculateFreight(Order order) reads only the postcode. Why it is wrong: couples the method to Order's structure; harder to reuse and test. Do instead: take the postcode (or a narrow interface).

7. Duplicated navigation logic

What happens: the same chain plus condition in many services. Why it is wrong: one structural change needs many edits; easy to miss one. Do instead: one method on the owner.

8. Lazy-loading through chains in loops

What happens: foreach (var o in orders) if (o.Customer.Address.State == "WA") with lazy loading. Why it is wrong: N+1 queries as well as coupling. Do instead: query with projection, or move logic to where the data is loaded.

9. Treating it as an absolute law

What happens: endless debates in review about every chain. Why it is wrong: it's a heuristic for coupling, not a compile rule. Do instead: use it to spot decisions in the wrong place.

10. Confusing queries with domain logic

What happens: EF LINQ navigation rewritten into awkward joins "to obey Demeter". Why it is wrong: query navigation is fine; it's translated to SQL. Do instead: focus on in-memory domain decisions.

🏛 Senior insight: trade-offs

I use the Law of Demeter as a smell detector rather than a law. Chains through domain objects tell me a decision lives in the wrong class.

Chain seen Usual fix
Service navigates entity graph for a rule Move the rule onto the aggregate
Method takes Order but uses only a postcode Take the postcode
Tests use nested mocks Introduce a direct collaborator
Mapper reads nested DTO properties Acceptable

🔗 Follow-ups the interviewer may ask

  1. When: Candidate moves decisions onto the owning object Why: Moves to Tell, Don't Ask
    How to answer

    Tell, Don't Ask means you tell an object what you want done, and the object decides how, using its own data. Instead of a service doing if (order.Status == Pending && !order.IsPaid) order.Status = Cancelled;, you call order.Cancel(), and the order enforces the rule. That keeps invariants in one place, removes duplicated checks across services and moves you from an anaemic model towards a rich one. It applies to behaviour; for reading data to display or report, querying is exactly right.

  2. When: Candidate says chains couple callers to object graphs Why: Probes broader coupling
    How to answer

    Coupling is the degree to which one module depends on another: how much it must know about it, and how likely it is to change when the other changes. Some coupling is unavoidable; the goal is coupling that is explicit, narrow and stable. In code the harmful kinds are reaching into another type's internals, shared mutable global state and passing control flags. In distributed .NET systems the expensive kinds are temporal coupling (synchronous call chains), shared databases and lock-step deployments. I reduce them with narrow interfaces, published contracts, asynchronous messaging where it fits, and each service owning its data.

  3. When: Candidate adds behaviour to entities Why: Checks rich versus anaemic models
    How to answer

    In an anemic domain model, entities are property bags with public getters and setters, and all business rules live in service classes that read and set those properties. In a rich domain model, entities own their behaviour: state is private, changes happen through methods like order.Pay(amount) that enforce invariants, and services only orchestrate (load, call, save). Anemic models scatter and duplicate rules, let any code put objects into invalid states, and are hard to test without a database. They are acceptable for simple CRUD or reporting with few rules. Move towards a richer model incrementally: make setters private, move one rule at a time into the entity, protect collections, and use value objects, guided by where bugs actually occur.

🛠 Real-world problems: how you handle them

Freight surcharge ignoring delivery address
Situation
A [retail] app read `order.Customer.Address.State` in [30] shipping rules.
Signals
Freight undercharged for [WA] deliveries after separate delivery addresses were introduced.
Investigation
Every rule navigated to the billing address; none knew about the new delivery address.
Resolution
Added `Order.DeliveryState` and `ShipsToRemoteState()`; rules now ask the order.
Prevention
Review guideline against service code navigating entity graphs; tests on the order's delivery logic.

How to tell it (STAR)

When we added separate delivery addresses, our freight rules kept using the billing state because [30] places navigated straight through the customer. I moved the decision onto the order itself, so callers ask the order where it ships. The bug was fixed in one place and future address changes stay inside the order.

↩ Usually asked after

More in Additional Principles

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me