Hyrum's Law: A Subtle Threat in Software Engineering
Hyrum's Law, named after Hyrum Wright, a software engineer at Google, states that "with a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody."
In simpler terms, no matter how well-defined your code is, other people will inevitably find ways to rely on undocumented behaviors, creating a hidden dependency that can constrain future changes.
This might seem like a minor concern, but for experienced software engineers, Hyrum's Law represents a significant challenge. It can lead to:
Rigidity: The need to maintain compatibility with unintended dependencies can make it difficult to evolve the system, hindering innovation and technical debt reduction.
Surprises: Unforeseen breakage due to changes in undocumented behaviors can lead to costly bug fixes and production downtime.
Technical Debt: The pressure to maintain compatibility with these hidden dependencies can lead to the accumulation of technical debt, making the codebase more complex and harder to maintain.
Examples of Hyrum’s Law in Action
To illustrate the effects of Hyrum’s Law, let’s look at a few simplified examples of how undocumented behaviors can create hidden dependencies and cause problems.
Example 1: Suppose you have a function that returns an array of numbers, and you decide to change the order of the elements in the array. This might seem like a harmless change, but it will break code that expects the original order of the elements.
function getNumbers() {
return [1, 2, 3, 4, 5];
}
$numbers = getNumbers();
// Prints 1
echo $numbers[0];
// Prints 5
echo $numbers[4];
// Modified the order of the elements
function getNumbers() {
return [5, 4, 3, 2, 1];
}
$numbers = getNumbers();
// Prints 5 instead of 1
echo $numbers[0];
// Prints 1 instead of 5
echo $numbers[4];
Example 2: You might think that adding a new field to the object that your function returns is a safe change, but it can break someone else’s code that relies on the object having a specific size or shape.
function getUser() {
return ["name" => "Alice", "age" => 25];
}
$user = getUser();
// Prints 2
echo count($user);
// Modified version with a new field
function getUser() {
return ["name" => "Alice", "age" => 25, "gender" => "female"];
}
$user = getUser();
// Prints 3 instead of 2
echo count($user);
Example 3: Changing the encoding of the string that your function returns from UTF-8 to UTF-16 may look like a minor change, but it will break anything that depends on the encoding of the string.
// Original UTF-8 return value
function getMessage() {
return "Hello, world!";
}
$message = getMessage();
// Prints 13
echo strlen($message);
// Modified return is UTF-16 encoded
function getMessage() {
return mb_convert_encoding("Hello, world!", "UTF-16");
}
$message = getMessage();
// Prints 26 instead of 13
echo strlen($message);
These are just a few examples of how Hyrum’s law can manifest in PHP. The main takeaway is that any observable behavior of your code can become an implicit dependency, and changing it can cause unexpected errors.
How to Minimize the Impact of Hyrum’s Law
Hyrum’s Law is inevitable, but not insurmountable. There are some best practices that all software engineers can follow to avoid or minimize its impact on their systems. Here are some of them:
Explicitly document guaranteed behaviors and explicitly exclude undocumented ones. This empowers everyone to make informed decisions and allows the original author to effectively communicate intent. Include tests, examples, and code comments (as needed) for clarity.
Leverage dependency injection: This technique allows for flexible injection of dependencies during runtime, enabling easier swapping of components and fostering loose coupling between modules.
Prioritize loose coupling: Aim for minimal dependencies between modules, allowing changes in one module to have minimal impact on others. This promotes independent development, testing, and deployment of components.
Design for isolation and decoupling: Encapsulate core functionalities within modules with minimal dependencies. This prevents changes in one area from causing unintended consequences in others, even if extension points are misused.
Standardize on interfaces and contracts: Clearly define interfaces and contracts between modules to ensure compatibility and facilitate the addition of new components without significant code modifications.
Continuously monitor and test: Regularly monitor system usage to identify unexpected behaviors arising from new features. Conduct thorough testing of new features to ensure they adhere to documented usage patterns.
Conclusion
Hyrum’s Law is a subtle but powerful threat that we as software engineers have to manage. It can create implicit dependencies that restrict our ability to make changes, leading to rigidity, surprises, and technical debt.
To combat Hyrum’s Law, we can follow some best practices, such as documenting our work, applying dependency injection, striving for loose coupling, designing for isolation, establishing interfaces and contracts, and monitoring and testing our system. By doing so, we can all create more robust, reliable, and adaptable systems.