Design, Simulate, Build

Published on Wednesday, January 7, 2026 - 9:30 PM

"Design, simulate, build" is a mantra for building reliable software. "Design" meaning to think about a problem before you act on it. "Simulate" meaning to build a "scale model" of the thing to validate the design. And "Build" meaning to actually go and build the final product.

"Design" and "Build" are fairly well known concepts. Engineers perform them to varying degrees of competency but, up to this point, they have been nearly impossible to avoid and so we have all done them.

"Simulate" I think will raise some eye brows. Not many people (in the web-dev domain at least) are aware of how to do this. But we do it implicitly when we write test coverage. Test coverage is, after all, a partial simulation of your program. A test-suite, therefore is a fragmented or incomplete simulation of your program.

Consider this. You've been asked by your manager to create an authentication system for your website. Let's take a moment to design a solution to this problem.

1. Accept a username and password.
2. Look up the user by their username.
3. Verify their password matches what's stored in the database.
4. Return some signed value which can be verified without the
   user's credentials.

This is an incomplete description of an authentication system but often our designs will neglect the full detail reality requires. The design then is a compass directing us to an outcome. It is not a step by step guide.

Happy with our design we present it to our manager. They like it and approve it or they request changes or reject the plan outright if some business objective can not be met by the talent the organization has on offer. The design phase gives us early feedback and can allow us to exit a problem without committing too many resources.

Let's assume our design was approved a simulate our design.

def authenticate(
    username: str,
    password: str,
    get_user: Callable[[str], User],
    verify_password: Callable[[str, str], bool],
    generate_token: Callable[[], str],
    sign_token: Callable[[str], str]
) -> str:
    user = get_user(username)
    if not user:
        raise Exception("invalid_grant")

    if not verify_password(user.password, password):
        raise Exception("invalid_grant")

    token = generate_token()
    signed_token = sign_token(token)

    return signed_token

Let's take a moment to reflect on this code. When we tried to apply our design we found we have undefined behavior. What do I do in the case of failure? The design doesn't account for that. Its in this moment you can return to the design, expand it, and present it to others or if these are mundate challenges, as is our case, handle them as we see fit.

This code perfectly encapsulates the logic of our authentication system without forcing us to reveal too much detail about the problem. Cleverly omitted is where a user is stored and how we retrieve it. Cleverly omitted is where the username and password come from. Cleverly omitted is what cryptography is needed to verify a password and sign a token. Cleverly omitted is what a token actually is in this system. Is it random? Does it need to be random?

Most clever of all is it allows us to answer questions (is my design plausible, does my design produce a useful program) without needing to understand databases, file-systems, web frameworks, or cryptography. Indeed the author of this simulation could be a blank slate and may need to learn all of these fields in order to actually build their program. But for now they can make progress and address these challenges piece-meal later.

Finally, let's (partially) build the software.

def get_user(username: str) -> User | None:
    # Ignore the SQL injection here.
    result = execute_sql(f"SELECT * FROM users WHERE user = '{username}'")

    if result:
        return map_result_to_user(result)
    else:
        return None

Notice the act of building the software is just the process of defining the callbacks requested by the authenticate function. Here I've opted to solve the problem of querying for a user. You can imagine I've solved all the other problems.

The solution presented is incorrect. We have a SQL injection vulnerability. Notice how solving that problem does not involve unwinding any aspect of the authenticate function. Indeed we can ignore the authenticate function entirely. It is irrelevant to the problem being solved here.

We have our first hint at re-use. Fetching a user is decoupled from our authenticate subroutine. This function can be passed to any subroutine which has a dependency on user data. This, as it turns out, is such a scalable idea that your program can be reduced to a series of reusable, declarative functions. Almost all problems in your software become questions of order and how outputs transform one another.

Conclusion

Simulation is a meaningful intermediary step that allows you to explore ideas without incurring the cost of fully solving a problem. It improves iteration speed, provides fast feedback, encourages exploration of the problem space with minimal cost, and offers the ability to progressively enhance a program as you build it. Finally, writing a program with simulation in mind yields re-use opportunities and coerces your program into a declarative format.