This blog has been migrated to linghao.io. Read this post on my new blog: https://linghao.io/notes/programming-beyond-practices/.
Programming Beyond Practices by Gregory Brown is one of the most insightful books I’ve read about software engineering. The book is consisted of 8 chapter-length stories that present a series of interesting topics that go beyond the sheer scope of programming.
This post is a refined version of the notes I took while reading this book.
Chapter 1: Using Prototypes to Explore Project Ideas
This chapter discusses how exploratory programming techniques can be used to build and ship a meaningful proof of concept for a product idea within hours after development begins.
Getting ideas out of a client’s head as quickly as you can
Conversations and wireframes (rough sketches) can be useful, but exploratory programming soon follows.
By getting working software into the mix early in the process, product design becomes an interactive collaboration and thus create faster feedback loops.
Start by understanding the needs behinds the project
Ask questions that reveal the goals of the people involved to both validate your assumptions and get more context on how other people see the problem.
Use wireframes to clearly communicate the basic structure of an application without getting bogged down in style details. The goal is to set expectations about functionality.
Set up a live test system as soon as you start coding
The initial setup doesn’t need to be production-ready. It just needs to be suitable for collecting useful feedback.
To speed things up, massive underinvestment is the name of the game here and it requires skill to pull it off.
It’s important to keep yourself from overthinking, because not even the most simple things survive first contact with the customer.
Discuss all defects, but be pragmatic about repairs
- It’s impossible to entirely guard yourself from making mistakes, but how you respond to them is critical.
Prototyping is about exploring a problem space, not building a finished product.
Focus on the risky or unknown parts of your work.
Check your assumptions early and often.
Limit the scope of your work as much as possible.
Remember that prototypes are not proudction systems. Make notes about any simplifying assumptions you made for maintainability.
Make WIP features a bit rough on purpose to prevent others from thinking they’re ready to ship.
Design features that make collecting feedback easy
Figuring out the right balance of when to play fast and loose and when to tighten things up.
E.g. In a simple video recommendation system based on tags (artist, genre, etc), it pays to display a sidebar that lists tags and their scores. Make the tags clickable so that the user can interact with it to figure out how watching videos with a certain tag influences its score and change the recommendation behavior.
E.g. Build an “import from CSV” feature so that the user can explore more with their custom data.
Encountering issues during prototyping is not a sign of a flawed process. It’s exactly what you should expect as a side effect of accelerated feedback loops. While they help you figure out how to build things faster, they also help you fail faster.
Chapter 2: Spotting Hidden Dependencies in Incremental Changes
This chapter discusses issues that can crop up whenever a production codebase is gradually extended to fit a new purpose.
There’s no such thing as a standalone feature
Don’t assume that a change is backward-compatible or safe just because it doesn’t explicitly modify existing features.
Pay attention to shared resources that live outside the codebase: storage mechanisms, processing capacity, databases, external services, libraries, user interfaces, etc. These tools form a “hidden dependency web” that can propagate side effects and failures between seemingly unrelated application features.
E.g. Suppose two seemingly independent Wiki sites share the same storage infrastructure. The first Wiki is only used by a few trusted admins, while the second Wiki is newly developed and made open to the public. An attack on the public Wiki could easily bring down the admin Wiki. This is an example of infrastructure-level dependency.
Use constraints and validations to help prevent local failures from causing global side effects. Also make sure to have good monitoring in place so that unexpected system failures are quickly noticed and dealt with.
E.g. Limit the number of pages allowed to be created on the public wiki.
E.g. Add monitoring to track Wiki page creation / deletion / editing and set up alerts for those events.
If two features share a screen, they depend on each other
- E.g. Adding a sidebar with flexible width to a Wiki page could make the actual content stuffed into a tiny column when the title of sidebar becomes too long.
Avoid non-essential real-time data synchronization
- E.g. Suppose that you need to display 5 most visited Wiki pages. Querying those in real-time is wasteful. And external service intergration are often full of headaches. Instead, you can write a script that periodically calls the analytics API and updates the 5 pages in a caching layer. By doing so, you sidestep the need to add further configuration information or libraries to the main web app.
Look for problems when code is reused in a new context
Watch out for context switches when reusing existing tools and resources. Any changes in scale, performance expectations, or privacy levels can lead to dangerous problems.
Focusing on superficial similarities of different use cases rather than their fundamental differences can cloud your judgement.
E.g. The admin Wiki uses a Markdown processor to render the page content, where editors are all trusted. But it’s certainly not safe for use by random individuals on the Internet.
Chapter 3: Identifying the Pain Points of Service Integrations
This chapter discusses some of the various ways that third-party systems can cause failures, as well as how flawed thinking about service integrations can lead to bad decision making.
Plan for trouble when your needs are off the beaten path
In theory, we should be approaching every third-party system with distrust until it is proven to be reliable. In practice, time and money constraints often cause us to drive faster than our headlights can see.
Be cautious when depending on an external service for something other than what it is well known for. If you can’t find many examples of others successfully using a service to solve similar problems to the ones you have, it is a sign that it may be at best unproven and at worst unsuitable for your needs.
Conduct smoke tests in such scenarios can help.
Remember that external services might change or die
- The key difference between libraries and services: a library can only cause breaking changes if your codebase or supporting infrastructure is modified, but an external service can break or change behavior at any point in time as it involves a remote system that isn’t under your control.
Look for outdated mocks in tests when services change
The presence of a decent test coverage can create an illusion of safety.
It’s possible that changes to a service dependency will render a mock object in tests outdated. When that happens the test result could be misleading.
Make sure that at least some of your tests run against live APIs of the services you depend upon.
Expect maintenance headaches from poorly coded robots
- You don’t need to just worry about your own integrations, it’s also essential to pay attention to the uninvited guests who integrate with you.
Remember that there are no purely internal concerns
A process can be well-intentioned but misguided or ill-executed.
Use every code review as an opportunity for a mini-audit of service dependencies — to evaluate testing strategy, to think through how failures will be handled, or to guard against misuse of resources.
Chapter 4: Developing a Rigorous Apporach Toward Problem Solving
This chapter discusses several straightforward tactics for breaking down and solving challenging problems in a methodical fashion. You should go read the book if you’re interested in the specific puzzle used for illustration.
Puzzles are awkward to use for developing practical coding skills but perfect for exploring general problem solving techniques.
Begin by gathering the facts and stating them plainly
- The raw materials of a problem description are often a scattered array of prose, examples, and reference materials. Make sense of it all by writing your own notes, then strip away noise until you are left with just the essential details.
Work part of the problem by hand before writing code
Behind each new problem that you encounter, there is a collection of simple sub-problems that you already know how to solve. Keep breaking things down into chunks until you start to recognize what the pieces are made of.
Challenging problems are made up of many moving parts. To see how they fit together without getting bogged down in implementation details, work through partial solutions on paper before you begin writing code.
Validate your input data before attempting to process it
- A valid set of rules operating on an invalid dataset can produce confusing results that are difficult to debug. Avoid the “garbage in, garbage out” effect by validating any source data before processing it.
Make use of deductive reasoning to check your work
Solve simple problems to understand more difficult ones
Chapter 5: Design Software from the Bottom Up
This chapter discusses a step-by-step approach to bottom-up software design, and examine the tradeoffs of this way of working. The example here is just-in-time production workflow. You should go read the book if you’re interested in pithy details of the whole approach.
Identify the nouns and verbs of your problem space
- List a handful of important nouns and verbs in the problem space. Then look for the shortest meaningful sentence that you can construct from the words on that list. Use that as the guiding theme for the first feature you implement.
Begin by implementing a minimal slice of functionality
Avoid unnecessary temporal coupling between objects
- As you continue to add new functionality into your project, pay attention to the connections between objects. Favor designs that are flexible when it comes to both quantities and timing so that individual objects don’t impose artificial constraints on their collaborators.
Gradually extract reusable parts and protocols
- When extracting reusable objects and functions, look for fundamental building blocks that are unlikely to change much over time, rather than looking for superficial ways to reduce duplication of boilerplate code.
Experiment freely to discover hidden abstractions
- Deferred decision making is an important part of bottom-up design. You will come to discover missing abstractions in the system as you proceed.
Know where the bottom-up approach breaks down
Take advantage of the emergent features that can arise when you reuse your basic building blocks to solve new problems. But watch out for excess complexity in the glue code between objects: messy integration points are a telltale sign that a bottom-up design style is being stretched beyond its comfort zone.
Top-down design and bottom-up design work like a spiral. Bottom-up design is good for exploring new areas and keeps things simple as you get some software up and running. When you hit dead ends or rough patches, then top-down mode is useful for considering the bigger picture and how to unify the connections between things.
Chapter 6: Data Modeling in an Imperfect World
This chapter discusses how small adjustments to the basic building blocks of a data model can fundamentally change how people interact with a system for the better.
Decouple conceptual modeling from physical modeling
In a system with messy data sources, it’s often better to preserve data in its raw form rather than attempting to transform it immediately into structures that closely map to domain-specific concepts. This way you get to keep some degree of flexibility by not imposing too much structure at the physical data modeling level. You can always process raw data into whatever form you’d like, but extracting that same information from complex models can be needlessly complicated.
E.g. In a time-tracking application, don’t model a work session as a pair of two punches. Instead, only record the raw punch data and use them to infer work sessions on-the-fly as needed.
Design an explicit model for tracking data changes
Reduce incidental complexity as much as possible by minimizing mutable state.
As you develop a data model, think through the ways data will be presented, queried, and modified over time.
Make it easy to preview, annotate, approve, audit, and revert transactional data changes in a human-friendly way. Implementing this type of workflow involves writing custom code rather than relying on pre-built libraries.
Applying the event sourcing pattern in your data models can help simplify things.
Understand how Conway’s Law influences data management practices
Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations – Melvin Conway.
Design data management workflows that respect and support the organizational culture of the people using your software. Otherwise the system could be crushed under the weight of a thousand workarounds.
Remember that workflow design and data modeling go hand in hand
Chapter 7: Gradual Process Improvement as an Antidote for Overcommitment
This chapter discusses some common anti-patterns that lead to struggles in software project management, and how incremental process improvements at all levels can alleviate some of those pains.
Respond to unexpected failures with swiftness and safety
- When dealing with system-wide outages, disable or degrade features as needed to get your software back to a usable state as quickly as possible. Proper repairs to the broken parts can come later, once the immediate pressure has been relieved.
Identify and analyze operational bottlenecks
Pay attention to the economic tradeoffs of your work
Look for areas where you are overcommitted and constrain them with reasonable budgets, so that you can free up time to spend on other work. Don’t rely solely on intuition for these decisions; use “back of the napkin” calculations to consider the economics of things as well.
E.g. Integrations with a whole list of advertising services takes up much time of the development. Yet the data shows that top 8 integrations alreay covers over 80% of growth.
Set a fixed limit on capacity (say 20%) that can be allocated to work on integrations each month. Simple time budgeting is a powerful tool for limiting the impact of high-risk areas of a project. It also encourages more careful prioritization and cost-benefit analysis.
Audit less popular integrations and decide what level of support to offer.
Reduce waste by limiting work in progress
Removing one bottleneck in a process will naturally cause another one to become visible.
Don’t get obsessed with the idea of catching up with the roadmap which is created before you ran into growing pains, as it could throw you back in a tough spot again.
Remember that unshipped code is not an asset; it’s perishable inventory with a cost of carry.
Help everyone in your projects understand this by focusing on what valuable work gets shipped in a given time period, rather than trying to make sure each person on the team stays busy.
Make the whole greater than the sum of its parts
- When collaborating with someone who works in a different role than your own, try to communicate in ways they can relate to.
Chapter 8: The Future of Software Development
This chapter is basically an imagination of what programming would look like in a future where the technology has advanced to a extent that we could focus purely on problem solving and communication rather than writing code.
There isn’t any takeaways in this chapter. You should go read the book if you’re interested in author’s depiction of the future. Instead, I’d like to quote some of author’s ending remarks to end this post.
Although the field of software development has a long way to go, I do expect things will get better in the years to come. It’s true that some folks among us are here solely for the tools, the code, the intellectual challenge of it all. But for the rest of us, that’s a matter of necessity and the environment we work in, not a defining characteristic of who we are.
My fundamental belief is that programmers are no less concerned for human interests than anyone else in the world; it’s just hard to make that your main focus in life when you spend a good portion of your day chasing down a missing semicolon, reading source code for an undocumented library, or staring at a binary dump of some text that you suspect has been corrupted by a botched Unicode conversion.
And my great hope is that if we fight against the influence of our rough, low-level, tedious tools and gradually replace them with things that make us feel closer to the outcome of our work, then our tech-centric industry focus will shift sharply and permanently to a humancentric outlook.