Principle of least knowledge: talk only to immediate collaborators; train-wreck chains; why fluent builders and LINQ are not violations; refactoring by moving behaviour.
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.
"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.Stateinside 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."
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 |
| 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 |
| 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 |
| 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 |
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.
Checks understanding of encapsulation and coupling at the call-site level.
Green flags
Red flags
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.
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?"
// 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.
| 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.
| 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.
grep for \.\w+\.\w+\.\w+ in domain/services folders, then review).Surcharge to use the billing state chain and see the wrong result.order.Customer.Address.City = ...) and move the command to the owner.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;
}
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.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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 |
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.
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.
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.
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.
Last updated 31 Oct 2026.