Programming Beyond Practices: Be More Than Just a Code Monkey

My personal notes on the book Programming Beyond Practices: Be More Than Just a Code Monkey.

Chapter 1. Using Prototypes to Explore Project Ideas

  • No matter what problem space you are working in, the first step is always to get ideas out of a client’s head and into the world as quickly as you possibly can.

  • Start by understanding the needs behind the project.

  • Use wireframes to set expectations about functionality.- Use wireframes to set expectations about functionality.

  • Set up a live test system as soon as you start coding.
    • Having a running system that everyone can interact with is essential; it promotes trying things out rather than simply talking about them, and makes it easier to share progress as you go.
    • The real goal of this first iteration is to set up a running system that will allow you to rapidly deploy new changes and kickstart the discovery process.
  • Discuss all defects, but be pragmatic about repairs
    • Whenever you find a flaw in your software, it is tempting to drop what you’re doing to work on repairs right away. But in the exploratory stages of a project, it’s important to balance the cost of each defect you encounter with the cost of the time it might take to fix it.
  • Check your assumptions early and often.
  • Limit the scope of your work as much as possible.
  • Remember that prototypes are not production systems.
  • Design features that make collecting feedback easy
    • Prototypes can help you figure out how to build useful things faster, but they also help you fail faster. If you can spot a dead-end path before you’ve already spent a ton of time walking down it, it means you can spend more energy on figuring out where the right path is.
  • Recommendations and reminders
    • Ask questions that reveal the goals of the people involved in a project. In doing so, you can both validate your assumptions and get more context on how other people see the problem.
    • Use wireframes (rough sketches) to clearly communicate the basic structure of an application without getting bogged down in style details.
    • Make sure to set up a live test system that everyone can interact with as soon as you start coding. The initial setup for this system needn’t be production-ready; it just needs to be suitable for collecting useful feedback.
    • In the early stages of a project, focus on the risky or unknown parts of your work.
    • Prototyping is about exploring a problem space, not building a finished product.

Chapter 2. Spotting Hidden Dependencies in Incremental Changes

  • There’s no such thing as a standalone feature.
  • If two features share a screen, they depend on each other.
    • Changes to database schemas always require some thought about data consistency. No matter how well isolated components are at the code level, there can still be hidden dependencies at the data layer.
  • Avoid non-essential real-time data synchronization.
  • Look for problems when code is reused in a new context.
    • Don’t assume that a change is backward-compatible or safe just because it doesn’t explicitly modify existing features. Instead, be on the lookout for hidden dependencies in even the most simple updates. Pay attention to the many shared resources that live outside your own 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.
    • Make use of constraints and validations to help prevent local failures from causing global side effects where you can. But also make sure to have good monitoring in place so that unexpected system failures are quickly noticed and dealt with. 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 if they aren’t carefully thought out.

Chapter 3. Identifying the Pain Points of Service Integrations

  • We should be approaching every third-party system with distrust until it is proven to be reliable.
  • Remember that external services might change or die.
  • Look for outdated mocks in tests when services change
  • Recommendations and reminders
    • 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.
    • Remember 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.
    • Watch out for outdated mock objects in tests whenever a change is made to a service dependency. To guard against potentially misleading test results, make sure that at least some of your tests run against the real services you depend upon.
    • Use every code review as an opportunity for a mini-audit of service dependencies — for example, to evaluate testing strategy, to think through how failures will be handled, or to guard against misuse of resources.

Chapter 4. Developing a Rigorous Approach Toward Problem Solving

  • Begin by gathering the facts and stating them plainly.
  • Work part of the problem by hand before writing code.
  • Validate your input data before attempting to process it.
  • Make use of deductive reasoning to check your work.
  • Solve simple problems to understand more difficult ones.
  • Recommendations and reminders
    • 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.
    • 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.
    • A valid set of rules operating on an invalid data set can produce confusing results that are difficult to debug. Instead of assuming that input data is clean, avoid the “garbage in, garbage out” effect by validating any source data before processing it.

Chapter 5. Designing Software from the Bottom Up

  • Identify the nouns and verbs of your problem space.
  • Begin by implementing a minimal slice of functionality.
  • Avoid unnecessary temporal coupling between objects.
  • The three essential quantities in software design: zero, one, and many.
  • Gradually extract reusable parts and protocols.
  • Experiment freely to discover hidden abstractions.
  • Recommendations and reminders
    • To get started on a bottom-up design, list a handful of important nouns and verbs in the problem space you are working in. Then look for the shortest meaningful sentence that you can construct from the words on that list. Use that sentence as the guiding theme for the first feature you implement. 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.
    • 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.
    • 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.

Chapter 6. Data Modeling in an Imperfect World

  • Decouple conceptual modeling from physical modeling.
  • Design an explicit model for tracking data changes.
  • 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)
  • Remember that workflow design and data modeling go hand in hand
  • Recommendations and reminders
    • Preserve data in its raw form, rather than attempting to transform it immediately into structures that closely map to domain-specific concepts. You can always process raw data into whatever form you’d like, but extracting that same information from complex models can be needlessly complicated.
    • As you develop a data model, think through the many different ways the data will be presented, queried, and modified over time. Very few real projects are limited to straightforward create, read, update, and delete operations on individual records… so plan accordingly.
    • 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, but applying the event sourcing pattern in your data models can simplify things a bit.
    • Design data management workflows that respect and support the organizational culture of the people using your software. Systems that do not take Conway’s Law into account tend to be crushed under the weight of a thousand workarounds.

Chapter 7. Gradual Process Improvement as an Antidote for Overcommitment

  • Respond to unexpected failures with swiftness and safety.
  • Identify and analyze operational bottlenecks.
  • Pay attention to the economic tradeoffs of your work.
  • Although often overlooked, simple time budgeting is a powerful tool for limiting the impact of the high-risk areas of a project, and also encourages more careful prioritization and cost-benefit analysis.
  • Reduce waste by limiting work in progress.
  • Make the whole greater than the sum of its parts
  • Recommendations and reminders
    • 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.
    • 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.
    • Remember that unshipped code is not an asset; it’s perishable inventory with a cost of carry.
    • Help everyone involved 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.
    • When collaborating with someone who works in a different role than your own, try to communicate in ways they can relate to. Take an outside view, and think, “What about this issue is relevant to the person I’m talking to? How does it fit into the bigger picture of the project?”