# Seeing Beyond the Code (Part 1) > 📚 Reading time: 15 minutes As a researcher, I've found that understanding software design has transformed my approach to foundational research, building products and how I communicate with engineering teams. This journey has pushed me to think harder than I have in a long time, and I hope sharing my experience helps you develop deeper skills in an area many research focused product and engineering professionals overlook. ## The Novel That Couldn't Be Fixed Let me share a story that perfectly illustrates the challenge of software design. Imagine I'm writing a novel and after months of hard work, I show it to my friend Jimmy. His verdict? "It's not good. The plot makes no sense, and the characters are cardboard." Determined to improve, I meticulously revise every page and bring it back. Jimmy's response? Exactly the same. Frustrated but not defeated, I pour over every paragraph, filling the pages with beautiful metaphors and stunning references. Yet when I return to Jimmy, his criticism hasn't changed at all. What's going wrong here? The problem lies in a fundamental mismatch between Jimmy's feedback and my approach. Plot and character don't exist on any individual page. There's no place in Harry Potter that explicitly states "Harry is brave" – this quality emerges from patterns across the entire work. Plot and character are organizing patterns that exist beyond the text itself while influencing every part of it. Think about the infamous final season of Game of Thrones. If we took those scripts and filmed them in a backyard with amateur actors, it would be terrible. But even with HBO's massive budget and production values, it was still widely criticized. Why? Because you cannot improve a story by polishing it one page at a time if the underlying structure is flawed. Software design works the same way. It doesn't exist in any single function or file – it's an organizing pattern that spans the entire codebase. Throughout this series, we'll dive into concepts like information hiding, coupling, and encapsulation. The challenge isn't just understanding these ideas superficially, but grasping them deeply enough to implement them without falling into subtle traps and edge cases. ### The Problem of Structural Integrity To extend our analogy, consider what makes a building stand. It's not just the quality of individual bricks or windows, but how they're arranged according to architectural principles. A beautiful facade can't save a structurally unsound building. In software, what we're truly designing is relationships – how modules interact, what information they share, and what assumptions they make about each other. These relationships form an invisible architecture that dictates how well the system can evolve, adapt to changing requirements, and resist errors. When legacy systems become difficult to modify, it's rarely because of poor coding style or outdated technology choices alone. It's because the relationships between components have degraded into a tangled web of interdependencies - what technical folk call "spaghetti code." ## Small Things That Know Little When engineering team asks for advice on their code, my response is deceptively simple: "Make smaller things that know less about each other." It's a principle that works equally well for microservices, UI components, or product features. But this immediately raises a fascinating question: what does it mean for code to "know" something? Some might say code knows about another module if it can access that module's internal state. But if we follow this logic, then purely functional programs with no state changes would have zero knowledge problems – which clearly isn't true. Others suggest code knows about a module if it uses functions from that module or imports it. But is it possible to remove all explicit references to a module and somehow make the knowledge problem worse? (Spoiler: yes, it is!) Can code know about things outside the program itself, like business processes or tax regulations? What about unchanging values like Pi? By the end of this article, you'll have a clear definition that answers all these questions and helps you identify and fix knowledge-related issues in your applications. ### Knowledge as Dependency In software design, when we say code "knows something," we're talking about dependencies. Module A depends on module B when changes to B might require changes to A. This dependency can take many forms: - **Implementation dependency**: A uses B's internal details (violating information hiding) - **Interface dependency**: A calls B's public API (normal and expected) - **Semantic dependency**: A relies on B's behavior, even without direct calls (subtle but important) - **Temporal dependency**: A depends on B's timing guarantees (often overlooked) David Parnas, in his seminal 1972 paper [On the Criteria to Be Used in Decomposing Systems into Modules](https://wstomv.win.tue.nl/edu/2ip30/references/criteria_for_modularization.pdf), introduced the concept of information hiding as a key principle for designing modules. He argued that modules should hide design decisions that are likely to change, exposing only stable interfaces to other parts of the system. Let's make this concrete with an example. Consider a product catalog that needs to display prices with tax included: ```python # Problematic implementation with high coupling def display_product_price(product): # Direct tax calculation within the display function tax_rate = 0.08 # Knowledge of the current tax rate price_with_tax = product["price"] * (1 + tax_rate) return f"${price_with_tax:.2f}" ``` This function "knows" too much - it has knowledge of how taxes are calculated and the current tax rate. When tax rules change, we'll need to find and update every function with this embedded knowledge. A better approach creates smaller things that know less: ```python # Tax calculation is a separate concern def calculate_tax_for_price(price, location): # Complex tax logic isolated here tax_rates = get_tax_rates_for_location(location) return price * (1 + tax_rates["combined"]) # Display only knows it needs a final price def display_product_price(price_with_tax): return f"${price_with_tax:.2f}" # Usage final_price = calculate_tax_for_price(product["price"], user["location"]) price_display = display_product_price(final_price) ``` Now each function knows less about the others, making them more reusable and easier to change independently. ## The Ghosts in Your Software Let's explore something peculiar about software through a practical example: a crash-safe file update function. The goal is straightforward – write data blocks to a file in a way that ensures either all writes complete or none do, even if the machine crashes mid-operation. My approach was to: 1. Create temporary files, 2. Write all data to them, 3. Rename them to the target files. Simple enough, right? But I've noticed intermittent corruption. The subtle problem was that the write operation is non-blocking. It doesn't actually perform the write; it merely schedules it to happen soon. This means the rename could complete before all writes finish – exactly what I was trying to avoid! To fix this, we need to call `fdatasync` to ensure all pending writes complete before the rename, and `fsync` afterward to ensure the rename completes before our function returns. But if you've ever read the manual pages for these functions, you know they're notoriously confusing, leading to endless debates about their exact behavior. The breakthrough comes when we shift how we think about system state. After a write operation, the state isn't simply "the disk after the write" – it's actually a set of possibilities where the write either has or hasn't completed yet. With multiple writes, this becomes a complex sequence where any number of them might have completed. With this mental model, we can precisely define what `fsync` does: it collapses all possible states into just one – the state where all pending operations have completed. These sequences of possible states don't physically exist like a file does. They're conceptual – like democracy or human rights. They're what philosopher Robert Pirsig might call "ghosts" in his classic "Zen and the Art of Motorcycle Maintenance." Newton's law of gravity is another such ghost – it doesn't physically exist, but it's a useful pattern we impose on the world to make sense of it. ### The Practical Reality of File System Guarantees To make this concrete, let's look at a real-world implementation of our crash-safe file update function: ```python def update_file_safely(file_path, new_data): # Create temporary file with unique name temp_path = f"{file_path}.{uuid.uuid4()}.tmp" try: # Write data to temporary file with open(temp_path, 'wb') as temp_file: temp_file.write(new_data) # Ensure data is physically written to disk # IMPORTANT: This is the key to crash safety temp_file.flush() # Flush to OS buffers os.fdatasync(temp_file.fileno()) # Flush OS buffers to disk # Atomically replace the original file with our temporary file os.rename(temp_path, file_path) # Ensure the directory entry is updated dir_fd = os.open(os.path.dirname(file_path), os.O_DIRECTORY) os.fsync(dir_fd) os.close(dir_fd) except Exception as e: # Clean up temporary file if anything goes wrong if os.path.exists(temp_path): os.unlink(temp_path) raise e ``` As explained in this article [Everything You Always Wanted To Know About fsync()](https://blog.httrack.com/blog/2013/11/15/everything-you-always-wanted-to-know-about-fsync/), the distinction between durability and atomicity is crucial here. The `fdatasync()` call ensures durability of the temporary file's contents, while the atomic `rename()` operation provides consistency. The final `fsync()` on the directory ensures the rename itself is durable. Without these synchronization points, we would be at the mercy of various buffers and caches throughout the system, with no guarantees about when (or if) our data would be safely on disk. ## The Three Dimensions of Understanding Software As a researcher and product manager with development experience, I've observed that we discuss software at three distinct levels. At the most concrete level – let's call it the **Runtime Dimension** – we're concerned with specific executions and inputs. This is what you see in a debugger: values, states, and sequences of events unfolding in real time. One level up is the **Implementation Dimension**, where we consider what happens across all possible inputs and environments. Here we talk about implementation details, code paths, and test coverage. But there's a higher plane – the **Conceptual Dimension**. This is where we discuss how we derived the code and what it's meant to do. At this level emerge the concepts of contracts between modules, assumptions being made, and principles of encapsulation. To illustrate the difference, imagine a function f that uses another function g: 1. At the Runtime Dimension, we might say: "When given input 42, F returns 84." 2. At the Implementation Dimension, we can say: "For all valid inputs, F produces correct outputs according to its specification." 3. But at the Conceptual Dimension, we can make a much more powerful statement: "For all valid inputs AND all valid changes to G, F still produces correct results." This perspective transformed my product management approach. Now when writing requirements, I focus not just on what features should do, but on the contracts between components. If the metadata service is allowed to return null (even if the current implementation never does), our filter should handle that possibility. ### Design By Contract: Making Interfaces Explicit These three dimensions connect directly to Bertrand Meyer's concept of [Design by Contract](https://www.itu.dk/~carsten/courses/opi-F08/DesignByContract.pdf), which he introduced in the Eiffel programming language. In this approach, software components interact through precisely defined contracts: - **Preconditions**: What the client must guarantee before calling a function - **Postconditions**: What the function guarantees after it completes - **Invariants**: What properties remain true throughout execution We will see the implementation of this in the following article. ### Abstractions at Different Scales These distinctions in understanding software apply at multiple scales of system design. Levels of abstraction form a hierarchy from high-level cross-regional architecture down to low-level implementation details: 1. **Cross-Regional Architecture**: How systems communicate across geographic boundaries 2. **Regional Clusters**: How services interconnect within a single region 3. **Service Architecture**: How a service is composed of applications 4. **Application Design**: How an application is organized into components 5. **Component Implementation**: How a component is implemented with specific code At each level, we can apply our three-dimensional understanding - from concrete runtime behavior to abstract logical guarantees. ## The Art of Coupling Management A core aspect of effective software design is managing coupling - how tightly components depend on each other. As explains in this [article](https://thevaluable.dev/cohesion-coupling-guide-examples/), there are various types of coupling, each with different impacts on system maintainability: 1. **Content Coupling**: One module directly accesses the internal implementation of another 2. **Common Coupling**: Multiple modules share global data 3. **Control Coupling**: One module controls the flow of another through flags or parameters 4. **Stamp Coupling**: Modules share complex data structures when they only need parts 5. **Data Coupling**: Modules share data through simple parameters (the ideal form) Reducing coupling while maintaining high cohesion (how well a module's elements work together) is the essence of good design. It's about creating boundaries that allow parts to evolve independently while still functioning as a coherent whole. This is where true modularity and robustness emerge – by separating the implementation of a component from its guarantees and contracts. And this is why software design exists only at this highest level of concepts, transcending specific code or features. ## Conclusion: From Concepts to Practice Throughout this exploration of software design, we've encountered various "ghosts" – conceptual patterns that don't exist in individual lines of code yet profoundly shape our systems. Like the plot of a novel or the structure of a building, these patterns emerge from relationships between components rather than from the components themselves. The most powerful lessons we can take from this journey are: 1. **Think in contracts, not implementations**. When designing interfaces between components, focus on the guarantees each side makes rather than how they fulfill those guarantees. This creates a stable foundation even as implementations evolve. 2. **Knowledge is dependency; minimize it thoughtfully**. Every piece of information one component has about another creates coupling. Make conscious decisions about what knowledge to share and what to hide. 3. **Move between dimensions of abstraction deliberately**. Learn to shift your perspective between runtime behavior, implementation details, and logical guarantees. Different problems require different levels of analysis. 4. **Cultivate an eye for invisible structures**. The most critical aspects of your system – its points of coupling, its contracts, its assumptions – are often invisible in the code itself. Develop the habit of making these explicit through documentation, tests, and clear boundaries. As you continue on your software design journey, your perception will evolve. You'll begin to see beyond individual lines and files to the architectural patterns that truly determine your system's resilience, adaptability, and coherence. These ghosts – the patterns, principles, and promises that define your architecture – are the essence of software design. ## Looking Ahead: From Theory to Practice In this first part of our series, we've laid the theoretical foundation for understanding software design as patterns that transcend individual code structures. In Part 2, "Understanding Program Logic," we'll move from theory to practice by exploring concrete techniques for implementing these concepts: 1. **Making the Three Dimensions Concrete** - We'll expand on the Runtime, Implementation, and Conceptual Dimensions with practical examples, showing how they manifest in everyday coding decisions and how to deliberately move between them. 2. **Implementing Design by Contract** - Building on the concept introduced here, we'll explore practical code techniques for expressing preconditions, postconditions, and invariants in modern programming languages. 3. **From Information Hiding to Contract Substitution** - We'll develop our understanding of knowledge as dependency by showing how proper interface design enables seamless component substitution without breaking client code. 4. **Making Invisible Structures Visible** - We'll demonstrate techniques for making the "ghosts" in your software explicit through executable assertions, property-based testing, and contract-based API design. 5. **Reasoning About Program Logic** - We'll introduce Hoare Logic and other formal methods for reasoning about program correctness, showing how they can be applied practically to everyday coding tasks. By connecting theory to practice across both parts, you'll develop not just an understanding of software design principles, but practical skills for applying them in your own work, whether you're a researcher, product manager, or engineer.