Skip to content
Leadership Garden Leadership
Garden

Software Engineering Laws - System Architecture

7 min read
Series Software Engineering Laws Part 5 of 11
Software Engineering Laws - System Architecture
Table of Contents

Conway’s Law

Any organization that designs a system will produce a design whose structure is a copy of the organization’s communication structure

Your software architecture will mirror your team structure. If you want microservices, organize teams around services. If you want monoliths, keep teams integrated.

Software boundaries form along lines of communication. If two teams have to file a Jira ticket and schedule three meetings to agree on an API change, they will naturally avoid creating new APIs. They’ll find ways to cram functionality into the existing, overloaded endpoints.

This law isn’t a curse; it’s a tool. It’s the basis for the “Inverse Conway Maneuver.” Want independent, scalable microservices? Create small, independent, full-stack teams, each with ownership over a specific business domain. Design your org chart to match the architecture you want.


Gall’s Law

A complex system that works has evolved from a simple system that worked

This law is a powerful argument for evolutionary design over grand, top-down creation. It asserts that you cannot successfully build a complex system in a single act of “Big Design Up Front.” The interdependencies are too numerous and the unknowns too great. All successful, large-scale systems begin as small, functional systems that incrementally grow in complexity over time, incorporating real-world feedback at each step.

Why it happens:

  • The Hubris of the Whiteboard: A whiteboard diagram cannot capture the nuance and friction of reality. It ignores network latency, database contention, API versioning, and a thousand other real-world constraints. Systems designed entirely in this sterile environment are brittle and collapse upon first contact with production traffic.
  • The Feedback Imperative: A simple, working system generates immediate feedback. You learn from its performance, its usage patterns, and its failure modes. This feedback is the most crucial input for guiding the system’s evolution. A “Big Design” approach operates in a feedback vacuum, guaranteeing its assumptions are wrong.
  • Complexity Hides Flaws: In a complex system designed from scratch, it is impossible to know if a fundamental architectural assumption is flawed until the very end, when the cost of changing it is astronomical. An evolutionary approach validates core assumptions early and often, when the cost of being wrong is low.

What to do about it:

  1. Build a “Walking Skeleton” First: Do not start by designing the entire cathedral. Start by building a simple, functioning shed. Your first goal should be to create the absolute simplest possible end-to-end implementation of your system that provides a tiny sliver of value. This might be a single API endpoint that touches the database and returns a hardcoded value. Get it deployed and working.
  2. Evolve, Don’t Build: Once your simple system is working, grow it incrementally. Add one feature, one integration, or one performance improvement at a time. Ensure the system remains stable and functional after each change. This iterative process allows complexity to emerge naturally and safely, guided by real-world needs and feedback.
  3. Prioritize Flow Over Features: In the early stages, your primary goal is not feature completeness but establishing a flow of value. Can you get code from a developer’s machine into production reliably? Can you monitor it? Can you get feedback? Optimizing this deployment and feedback loop is far more important than building out the perfect architecture diagram.
  4. Embrace Refactoring as a Core Practice: In an evolutionary system, refactoring is not a sign of failure; it is a sign of health. As the system grows and you learn more, you will need to revisit and improve earlier design decisions. This continuous refinement is the engine of sustainable evolution, allowing you to adapt the system without starting over.

Hyrum’s Law

With a sufficient number of users of an API, all observable behaviors will be depended on by somebody

Also known as The Law of Implicit Interfaces, this is a sobering rule for anyone who maintains a public-facing system. It states that the effective contract of your API is not what you write in your documentation, but the sum of all its observable behaviors. This includes performance characteristics, the exact text of error messages, and even your bugs. If a user can observe it, someone, somewhere, will build a dependency on it.

Why it happens:

  • Pragmatism over Purity: Users are trying to solve their problems as quickly as possible. If they discover that your API accidentally returns an extra, undocumented data field that saves them from making another call, they will use it. They don’t care that it’s not in the official contract; they care that it works.
  • Accidental Features: What you see as a bug, a user might see as a valuable feature. For example, if an endpoint consistently returns items in a specific sorted order due to an implementation detail, users will build UIs that rely on that order, breaking spectacularly when you later “fix” it by changing the sort logic.
  • The Law of Large Numbers: At scale, even the most obscure edge case will be encountered and exploited. With millions of API calls, every observable quirk of your system becomes part of someone’s critical path.

What to do about it:

  1. Minimize Your Public Surface Area: The most effective defense is to expose as little as possible. Design minimalist APIs that return only the data specified in the contract. Keep implementation details (like internal methods or data structures) strictly private. If users can’t see it, they can’t depend on it.
  2. Be Explicit and Strict in Your Contracts: Use strong schemas (e.g., OpenAPI, GraphQL schemas) to rigorously define your API’s inputs and outputs. This makes the intended contract machine-readable and clear. Being strict in what you return prevents accidentally exposing new behaviors.
  3. Version Aggressively: When you must make a change that could violate an implicit dependency—even if it’s fixing a bug—treat it as a breaking change. Introduce the fix in a new version of the API (/v2/). This allows legacy users to continue relying on the old behavior while new users adopt the corrected version.
  4. Monitor Usage Before Changing Anything: Before “fixing” any behavior, use logging and analytics to understand how your API is actually being used in the wild. You may be surprised to find that a significant number of users rely on the very quirk you intend to remove. Communication and long deprecation windows are essential.

Postel’s Law

Be conservative in what you do, be liberal in what you accept from others

Often called the Robustness Principle, this is a fundamental design guideline for building resilient and cooperative networked systems. It prescribes a two-part philosophy for communication: be absolutely precise and conformant in the data you emit, but be as flexible and tolerant as possible when interpreting the data you receive. This creates systems that are easy to rely on, yet forgiving of others’ imperfections, leading to more stable integrations overall.

Why it happens:

  • Systems are Heterogeneous: In any distributed environment, you will be communicating with services written by different teams, in different languages, on different schedules. Expecting every client to perfectly adhere to your specification at all times is unrealistic and leads to brittle connections.
  • Preventing Cascading Failures: A service that is overly strict in what it accepts becomes a weak link. If it rejects a request due to a trivial, non-breaking formatting error (like an extra, unknown field), it can cause a failure in the client system. A more liberal service can ignore the minor error and fulfill the request, keeping the entire workflow alive.
  • Fostering Future-Proofing and Evolution: A client built to be liberal in what it accepts won’t break when a server adds new, optional fields to its response. A server that is liberal won’t break when a client sends extra context. This dynamic allows systems to evolve independently without constantly shattering their integrations.

What to do about it:

  1. On Sending (Be Conservative): Adhere meticulously to your published specification. If your API contract says a field is an integer, send an integer, not a string. Do not add new fields to a response without versioning the API. Your goal is to be a predictable, reliable, and “boring” producer of data so that your consumers can trust you completely.
  2. On Receiving (Be Liberal): Design your parsers and validation logic to be tolerant. Ignore unknown fields in a JSON payload rather than rejecting the entire request. If the user’s intent is clear, try to accommodate minor variations (e.g., accepting a date in a few common formats). Provide sensible defaults for any optional or missing data.
  3. Know the Boundary of Liberality: This principle is not an excuse for sloppy design or accepting ambiguous input. Be liberal with formatting, but remain strict with semantics. For example, you might accept an extra, harmless field in a request, but you must firmly reject a request that is semantically invalid or could lead to data corruption (e.g., an e-commerce order with a negative quantity). The goal is robustness, not recklessness.
Share

Series

Software Engineering Laws

A practical sequence on the recurring laws and constraints that shape engineering work, from coding and architecture to testing and performance.

Open series page

Explore further

Keep going with a few related posts, then branch into the topic hubs and collections around the same ideas.

Continue with these