Loading content...
Consider walking into a friend's house and, instead of asking your friend to make you a cup of coffee, you wander into their kitchen, open their cabinets, find their coffee maker, rummage through their drawers for filters, and start brewing yourself. In the physical world, this behavior is considered bizarre, intrusive, and a violation of social norms.
Yet this is exactly how most software is written.
Objects routinely reach through other objects, navigate deep into their internal structures, manipulate things they don't own, and make assumptions about implementation details they have no business knowing. The result? Tightly coupled systems that resist change, propagate failures in unexpected ways, and become maintenance nightmares.
The Law of Demeter addresses this problem directly. It's a design guideline that formalizes what we intuitively understand in social contexts: you should only interact with things you directly know about, not the friends of your friends.
By the end of this page, you will understand the formal definition of the Law of Demeter, the intuitive mental model behind it, why it was formulated, and how it fundamentally shapes the way objects should interact in well-designed systems. You'll learn to recognize LoD as not merely a coding style preference but as a profound architectural principle with far-reaching implications for maintainability, testability, and system evolution.
The Law of Demeter, also known as the Principle of Least Knowledge, was formulated in 1987 by Ian Holland at Northeastern University while working on the Demeter Project—a research initiative focused on aspect-oriented programming and adaptive software. The name "Demeter" comes from the Greek goddess of agriculture, reflecting the project's focus on "growing" software in an adaptive manner.
The principle emerged from Holland's observation that systems with limited, focused object interactions were significantly easier to modify, debug, and extend than systems where objects freely navigated complex object graphs.
The Law of Demeter for Methods states that a method M of object O should only call methods belonging to:
In essence: An object should only talk to its immediate friends, never to strangers.
Understanding "Friends" vs "Strangers":
Friends are objects that a method legitimately knows about: its own object, its parameters, objects it creates, and direct collaborators held as instance fields.
Strangers are objects obtained through friends—the results of method calls on friends, especially those that return internal objects that the caller then manipulates.
The critical insight is that strangers are not your responsibility. When you interact with a stranger, you're making assumptions about its interface, its behavior, and crucially, its continued existence and structure. These assumptions create hidden dependencies that propagate throughout the codebase.
12345678910111213141516171819202122232425
// ❌ VIOLATING the Law of Demeter// This method interacts with "strangers" — objects it doesn't directly know class OrderProcessor { processOrder(customer: Customer): void { // We get the wallet (stranger) through the customer (friend) // Then we reach into the wallet to get the credit card (stranger's friend) // Then we reach into the credit card to get the limit (stranger's friend's detail) const creditLimit = customer.getWallet().getCreditCard().getLimit(); if (creditLimit > this.orderTotal) { // We're navigating even deeper into unknown territory customer.getWallet().getCreditCard().charge(this.orderTotal); // And again, reaching through multiple objects customer.getAddress().getCity().getShippingZone().calculateDeliveryTime(); } }} // What's wrong here?// 1. OrderProcessor knows about Wallet, CreditCard, Address, City, ShippingZone// 2. If ANY of these classes change their structure, OrderProcessor breaks// 3. OrderProcessor cannot be tested without all these collaborators// 4. The coupling tree is deep and fragile1234567891011121314151617181920
// ✅ FOLLOWING the Law of Demeter// This method only talks to its direct friends class OrderProcessor { processOrder(customer: Customer): void { // Ask the customer directly — they handle their own internals if (customer.canAfford(this.orderTotal)) { customer.charge(this.orderTotal); // Ask the customer about delivery — they know their own address const deliveryTime = customer.estimateDeliveryTime(); } }} // What's better here?// 1. OrderProcessor only knows about Customer// 2. Customer's internal structure can change freely// 3. OrderProcessor can be tested with a simple Customer mock// 4. The coupling is shallow and resilientWhile the formal definition provides precise rules, the Law of Demeter is best understood through a mental model of trust boundaries. Every object in your system has a boundary—a clear demarcation between what it knows about and what it doesn't.
When you violate LoD, you're reaching across trust boundaries into territory you shouldn't know about. You're making yourself responsible for things that aren't your concern, and you're binding yourself to implementation details that may change.
The "Don't Ask, Tell" Perspective:
LoD is intimately connected to the principle of "Tell, Don't Ask" (which we'll cover later in this chapter). When you violate LoD, you're typically asking an object for its internals so you can make decisions yourself. When you follow LoD, you tell the object what you need and let it figure out how to accomplish it.
This shift from interrogation to delegation is fundamental to object-oriented design. Objects should be autonomous agents that handle their own responsibilities, not passive data containers that expose their guts for external manipulation.
Imagine you want to know if your neighbor received their newspaper today. Violating LoD would be: walking into their house, finding their bedroom, locating their nightstand, checking if a newspaper is on it. Following LoD would be: asking your neighbor directly, "Did you get your newspaper today?" You don't need to know their house layout, and if they rearrange their furniture, your question still works.
Let's examine each of the five categories of "friends" that the Law of Demeter permits you to interact with. Understanding these categories precisely is essential for correctly applying the principle.
this.calculateTotal() — always allowed123456789101112131415161718
class ShoppingCart { private items: CartItem[] = []; calculateTotal(): number { // ✅ Calling another method on 'this' — always allowed return this.items.reduce((sum, item) => sum + this.calculateItemPrice(item), 0); } private calculateItemPrice(item: CartItem): number { // ✅ Using 'this' to access discount logic return item.basePrice * this.getDiscountRate(); } private getDiscountRate(): number { // ✅ Self-interaction is always permissible return this.items.length > 5 ? 0.9 : 1.0; }}123456789101112131415
class PaymentProcessor { // 'paymentMethod' is a parameter — it's a friend processPayment(order: Order, paymentMethod: PaymentMethod): void { // ✅ Calling methods on the parameter 'paymentMethod' — allowed if (paymentMethod.isValid()) { // ✅ Calling methods on the parameter 'order' — allowed const amount = order.getTotal(); paymentMethod.charge(amount); } // ❌ But this would violate LoD: // order.getCustomer().getEmail().getDomain().validate(); // We're reaching through 'order' to objects we weren't introduced to }}123456789101112131415161718
class ReportGenerator { generateReport(data: ReportData): void { // ✅ We create the formatter — it's our friend const formatter = new ReportFormatter(); // ✅ We create the output — it's our friend const output = new StringBuilder(); // ✅ Interacting with objects we created is always allowed output.append(formatter.formatHeader(data.getTitle())); output.append(formatter.formatBody(data.getContent())); // ✅ We create the file writer — it's our friend const writer = new FileWriter("/reports/output.txt"); writer.write(output.toString()); writer.close(); }}1234567891011121314151617181920212223242526
class EmailService { // These are direct components — permanent friends private readonly templateEngine: TemplateEngine; private readonly smtpClient: SmtpClient; private readonly logger: Logger; constructor( templateEngine: TemplateEngine, smtpClient: SmtpClient, logger: Logger ) { this.templateEngine = templateEngine; this.smtpClient = smtpClient; this.logger = logger; } sendWelcomeEmail(user: User): void { // ✅ Interacting with our component objects — allowed this.logger.info("Sending welcome email"); const body = this.templateEngine.render("welcome", { name: user.getName() }); // ✅ Using user (parameter) and smtpClient (component) this.smtpClient.send(user.getEmail(), "Welcome!", body); }}While the original Law of Demeter permits interacting with globally accessible objects, modern design wisdom discourages this practice. Globals create hidden dependencies, make testing difficult, and introduce implicit coupling. In contemporary practice, dependency injection is preferred over global access.
You may have heard the Law of Demeter summarized as the "One Dot Rule" or "No Method Chaining": only use one dot in a method call chain.
This simplification is:
Let's understand when this heuristic helps and when it misleads.
1234567891011121314151617181920212223242526272829303132
// The "One Dot Rule" says these are suspicious: // ❌ Clear LoD violations (multiple dots reaching through objects)customer.getWallet().getCreditCard().charge(amount);order.getShippingAddress().getCity().getTimeZone();employee.getDepartment().getManager().getEmail(); // ✓ These would pass the "One Dot Rule"customer.charge(amount);order.getDeliveryTimeZone();employee.getManagerEmail(); // BUT: Not all multi-dot expressions violate LoD! // ✅ Fluent interfaces return 'this' — NOT an LoD violationqueryBuilder .select("name", "email") .from("users") .where("active", true) .orderBy("created_at") .limit(10); // ✅ Building up a result from your own creation — NOT an LoD violationnew StringBuilder() .append("Hello, ") .append(name) .append("!") .toString(); // ✅ Navigating a collection you received — context-dependent// If 'items' is a parameter or component, this may be fineitems.filter(i => i.isActive()).map(i => i.getName());Instead of counting dots, ask: "Am I reaching through an object to get to something inside it that I shouldn't know about?" If you're navigating into the internal structure of another object to access its collaborators, you're likely violating LoD. If you're working with an API designed for chaining (like fluent builders) or processing collections, you may be fine.
| Pattern | Why It's OK | Example |
|---|---|---|
| Fluent Interfaces | Each method returns 'this' — no navigation through internals | builder.withName('x').withAge(20).build() |
| Method Chaining on Created Objects | You created it, so you own it | new Date().toISOString().substring(0, 10) |
| Stream/Collection Processing | You're transforming data you own | list.filter().map().sort() |
| Optional Unwrapping | Intentional null-safe navigation | Optional.of(x).map(fn).orElse(default) |
| DSL/Query Builders | Designed for chained configuration | query.select().from().where() |
The Law of Demeter isn't just a stylistic preference or a rule for rule's sake. It addresses fundamental problems in software design that surface as systems grow and evolve. Understanding the why behind LoD makes its application more intuitive.
12345678910111213141516171819202122232425262728
// LoD violation: OrderService is coupled to 5 classesclass OrderService { processOrder(order: Order): void { // This single line creates dependencies on: // 1. Order // 2. Customer (returned by getCustomer) // 3. Wallet (returned by getWallet) // 4. CreditCard (returned by getCreditCard) // 5. Bank (returned by getBank) order.getCustomer().getWallet().getCreditCard().getBank().validate(); }} // If Bank changes its API: OrderService breaks// If CreditCard changes how it references Bank: OrderService breaks// If Wallet changes how it stores CreditCard: OrderService breaks// If Customer changes how it stores Wallet: OrderService breaks // LoD-compliant: OrderService is coupled to 1 classclass OrderService { processOrder(order: Order): void { // Only depends on Order — Order handles the rest internally order.validatePayment(); }} // Changes to Customer, Wallet, CreditCard, Bank don't affect OrderService// Only Order knows about its internal payment validation logica.getB().getC(), you're exposing that A contains B, and B contains C123456789101112131415161718192021222324252627
// Testing LoD-violating code is painfuldescribe('OrderService with LoD violation', () => { it('processes order', () => { // We need to create entire mock chains const mockBank = { validate: jest.fn() }; const mockCreditCard = { getBank: () => mockBank }; const mockWallet = { getCreditCard: () => mockCreditCard }; const mockCustomer = { getWallet: () => mockWallet }; const mockOrder = { getCustomer: () => mockCustomer }; orderService.processOrder(mockOrder); expect(mockBank.validate).toHaveBeenCalled(); });}); // Testing LoD-compliant code is simpledescribe('OrderService with LoD compliance', () => { it('processes order', () => { // Just mock the direct collaborator const mockOrder = { validatePayment: jest.fn() }; orderService.processOrder(mockOrder); expect(mockOrder.validatePayment).toHaveBeenCalled(); });});When you violate LoD, any change along the access chain ripples back to the caller. A small internal refactoring in a distant class can break code that never should have known about that class in the first place. LoD creates firebreaks that contain change impact.
Developing an intuition for LoD violations requires practice. Here are common patterns that signal potential problems, along with questions to ask yourself when reviewing code.
a.getB().getC().getD().doSomething() — reaching deep into object graphsif (a.getB() != null && a.getB().getC() != null...) — defensive navigation123456789101112131415161718192021222324252627282930313233
// Common violation patterns to recognize // Pattern 1: "Train Wreck" — long chains of gettersconst city = order.getCustomer().getAddress().getCity(); // ❌ // Pattern 2: "Interrogation" — extracting data for external decisionsif (customer.getAccount().getBalance() > amount) { // ❌ customer.getAccount().debit(amount);} // Pattern 3: "Middle Man Bypass" — skipping encapsulationinvoice.getLineItems().get(0).getProduct().getSupplier().notify(); // ❌ // Pattern 4: "Hidden Cost" — null checks reveal deep couplingif (user != null && user.getProfile() != null && user.getProfile().getSettings() != null) { // ❌ applySettings(user.getProfile().getSettings());} // Pattern 5: "Feature Envy" — method knows too much about othersclass OrderPrinter { print(order: Order): void { // This method knows way too much about Order's internals const customer = order.getCustomer(); const address = customer.getAddress(); const lines = order.getLineItems(); lines.forEach(line => { const product = line.getProduct(); const price = product.getPrice(); // ... this is Order's job, not OrderPrinter's }); }}The Law of Demeter is deeply connected to the "Tell, Don't Ask" principle (which we'll explore in depth later in this chapter). Understanding this connection clarifies both principles.
When you violate LoD, you're almost always "asking" instead of "telling":
When you follow LoD, you're "telling" your immediate friend:
1234567891011121314151617181920212223242526272829303132333435363738
// "Asking" pattern — violates both LoD and Tell-Don't-Askclass PaymentService { processRefund(order: Order): void { // Ask for the payment method const payment = order.getPayment(); // Ask for the gateway const gateway = payment.getGateway(); // Ask for the original transaction const transaction = payment.getTransaction(); // Finally tell the gateway to do something gateway.refund(transaction.getId(), transaction.getAmount()); }} // "Telling" pattern — follows both LoD and Tell-Don't-Askclass PaymentService { processRefund(order: Order): void { // Tell the order to handle the refund // Order knows how its payment works internally order.processRefund(); }} // Inside Order, the same principle appliesclass Order { processRefund(): void { // Tell payment to handle refund — Order doesn't reach into Payment this.payment.refund(); }} // Inside Payment, the same principle continuesclass Payment { refund(): void { // Tell gateway to refund — Payment handles its own internals this.gateway.refund(this.transaction.getId(), this.transaction.getAmount()); }}When you follow LoD, you naturally push behavior closer to the data it operates on. Instead of extracting data from objects and manipulating it externally, you ask objects to perform operations themselves. This leads to richer domain models where objects have meaningful behavior, not just getters and setters.
We've established the foundational understanding of the Law of Demeter. Let's consolidate what we've learned before moving on to explore how LoD reduces coupling in practice.
Now that we understand what the Law of Demeter is and why it matters, the next page will dive deep into how LoD reduces coupling in practice. We'll examine concrete scenarios where LoD violations cause real problems, and how applying LoD creates more resilient, maintainable systems.