Locality of Behavior


Introduction#

Locality of Behavior states that the behavior of a unit of code should be as obvious as possible by looking only at that unit of code. This principle prioritizes explicit, immediate understanding over abstract elegance. When you can understand what code does without jumping between multiple files or hunting through layers of abstraction, you reduce the cognitive load required for maintenance and debugging.

Origin and Context#

The term “Locality of Behavior” was coined by Carson Gross, creator of the htmx library, though the underlying ideas have existed in various forms throughout software development history. Gross articulated this principle as part of his broader critique of modern web development’s tendency toward excessive abstraction and indirection.

The principle emerged from observing how contemporary frameworks often scatter simple functionality across multiple files, configuration systems, and abstraction layers. While these patterns might feel sophisticated, they frequently make codebases harder to understand and maintain rather than easier.

Similar concepts appear throughout software engineering literature under different names. The principle of least surprise, which favors predictable behavior over clever abstractions, shares common ground with locality of behavior. The Unix philosophy of “do one thing and do it well” also emphasizes clarity and directness over complex abstractions.

Gross’s formulation specifically addresses the modern tendency to treat abstraction as inherently virtuous, regardless of whether it improves or hinders understanding. Locality of Behavior provides a framework for evaluating when abstraction truly serves maintainability versus when it creates unnecessary cognitive overhead.

The Cost of Indirection#

Modern software development often celebrates abstraction and indirection as signs of sophisticated design. Dependency injection, extensive inheritance hierarchies, and elaborate configuration systems are frequently treated as architectural achievements. However, each layer of indirection creates a cognitive tax that developers must pay every time they interact with the code.

Consider this seemingly clean dependency injection example:

class OrderService {
  constructor(
    private validator: IValidator,
    private repository: IRepository,
    private emailService: IEmailService,
    private logger: ILogger
  ) {}

  async processOrder(order: Order): Promise<void> {
    await this.validator.validate(order);
    await this.repository.save(order);
    await this.emailService.send(order.customerEmail);
    this.logger.info(`Order ${order.id} processed`);
  }
}

This code appears clean and follows conventional dependency injection patterns. However, understanding what actually happens when processOrder executes requires navigating through four different interfaces and their implementations. A simple bug investigation becomes an archaeological expedition through multiple files, interfaces, and configuration mappings.

Explicit Over Abstract#

Locality of Behavior suggests that explicitness often serves maintainability better than abstraction. Consider this illustrative alternative approach:

class OrderService {
  async processOrder(order: Order): Promise<void> {
    // Validate order
    if (!order.customerEmail || !order.items.length) {
      throw new Error('Invalid order data');
    }

    // Save to database
    const query = 'INSERT INTO orders (id, email, items) VALUES (?, ?, ?)';
    await db.execute(query, [order.id, order.customerEmail, JSON.stringify(order.items)]);

    // Send confirmation email
    const emailBody = `Your order ${order.id} has been processed.`;
    await fetch('/api/email', {
      method: 'POST',
      body: JSON.stringify({ to: order.customerEmail, body: emailBody })
    });

    // Log completion
    console.log(`Order ${order.id} processed at ${new Date().toISOString()}`);
  }
}

This version eliminates abstraction layers in favor of explicit behavior. Every action the method performs is visible immediately. When debugging a failed order processing, you can see exactly what validation occurs, which database query executes, and how the email gets sent, all within a single method.

When Abstraction Creates Distance#

The software industry has developed a cultural bias toward abstraction that sometimes works against maintainability. Framework enthusiasts often create elaborate systems where simple operations become distributed across multiple files and layers. This creates what Gross describes as “high cognitive overhead” where understanding basic functionality requires mental juggling of multiple concepts simultaneously.

Consider a typical modern web application where handling a user login requires understanding middleware chains, dependency injection containers, route handlers, service layers, repository patterns, and configuration files. The actual business logic becomes obscured behind architectural ceremony.

Locality of Behavior suggests that this complexity often serves the architecture more than it serves the developers who must maintain the system. When the cost of understanding exceeds the benefit of flexibility, abstraction becomes a liability rather than an asset.

The Maintenance Advantage#

Code optimized for locality of behavior creates significant advantages during maintenance phases, which consume the majority of a software system’s lifecycle. When behavior is localized, debugging becomes straightforward. You can trace execution paths without constantly context switching between files. Performance optimization becomes more direct because you can see exactly what operations occur and in what order.

Consider how much easier it is to optimize the explicit processOrder method versus the abstracted version. In the explicit version, you can immediately see that database operations and email sending happen sequentially. You might recognize that these could run in parallel or that the email could be queued for later processing. In the abstracted version, you would first need to understand the implementation details of four different services before making similar optimizations.

Balancing Locality with Other Concerns#

Locality of Behavior does not advocate for eliminating all abstraction or creating monolithic functions. Instead, it suggests that the default should favor explicitness over abstraction, and that abstractions should pay for themselves through clear benefits that outweigh their cognitive costs.

Appropriate abstractions might include utilities that genuinely simplify complex operations, domain objects that capture important business concepts, or interfaces that enable meaningful polymorphism. The key is ensuring that each abstraction makes the code easier to understand, not harder.

// Good abstraction: simplifies complex operation
function formatCurrency(amount: number, currency: string): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency
  }).format(amount);
}

// Usage is clear and behavior is obvious
const price = formatCurrency(29.99, 'USD');
console.log(`Total: ${price}`);

This abstraction improves locality because it makes the intent clearer at the call site while encapsulating complexity that doesn’t need to be understood every time the operation is used.

Practical Application#

To apply Locality of Behavior effectively, start by questioning whether each abstraction in your codebase truly improves understanding. Ask whether someone unfamiliar with the system could understand what a function does by reading only that function, or whether they need to understand multiple other components first.

When writing new code, default toward explicit implementations and only introduce abstractions when they solve real problems that justify their cognitive cost. When reviewing code, consider whether proposed abstractions make the code easier or harder to understand for someone encountering it for the first time.

Remember that future developers, including yourself six months from now, will appreciate code that reveals its behavior directly rather than hiding it behind layers of indirection. The time saved during initial development by copying a few lines of code is often recovered many times over during the maintenance phase.

Conclusion#

Locality of Behavior provides a practical framework for making trade-offs between abstraction and explicitness. By prioritizing immediate understanding over architectural elegance, it helps create codebases that are easier to debug, optimize, and maintain.

The principle doesn’t eliminate abstraction but rather establishes a higher bar for when abstraction is justified. Good abstractions should make code easier to understand, not create additional cognitive load through unnecessary indirection. When behavior is local and explicit, software becomes more maintainable and developers can focus on solving business problems rather than navigating architectural complexity.