Software Design for Startups (Notes)

I’ve been trying to reduce the complexity of our codebase to double-down on our reliability. I read the book A Philosophy of Software Design and I selectively read a few chapters from the books Clean Code and The Pragmatic Programmer. Here are some notes I have on the topic – at the end I propose a software design philosophy for startups.

Crucial Notes from A Philosophy of Software Design

  • Complexity is the fundamental challenge of software engineering. It emerges from change amplification (a change in one place requires multiple changes elsewhere), cognitive load (a programmer has to keep several things in mind while working), unknown unknowns (the most dangerous kind of complexity; programmers don’t know what they need to know before making a change).
  • Modules should be deep: envisioning an interface as an edge of a rectangle; the interface should be narrow and the area of the rectangle (the underlying module) should have a lot of area. In other words, interfaces should be simple relative to the complexity they abstract away.
  • Change amplification seems to be a rephrasing of tight coupling.
  • Documentation helps with unknown unknowns. Code can often also be written better to avoid these issues. Example of unknown unknowns: modifying code requires a change to a Dockerfile.

Crucial Notes from Clean Code

  • Functions should be around 20 lines long, they should contain logic that’s at the same level of abstraction (for example, they shouldn’t mix high-level service orchestration with low-level invariant checks), they should have one reason to change (Single Responsibility Principle), and they should be passed a low number of arguments.

OOP is Great

I once believed that OOP was additional boilerplate and complexity. But it’s great for these reasons:

  • (Lol) Calling a method is prefixed by the class the method belongs to
  • Private variables make enforcing state invariants local to a class
  • Interfaces make the Dependency Injection design pattern possible

Requirements-Driven Design

Requirements-Driven Design seems kind of obvious to do. The process is:

  • Getting clear on the requirements for a task
  • Splitting the requirements into code responsibilities
  • Assigning the responsibilities into Object Stereotypes
  • Finding more Object Stereotypes for collaboration between these responsibilities

Design Patterns

Here’s a rundown on useful design patterns for reducing complexity.

  • Decorator Pattern: Starting from an interface, add wrappers to the interface to add functionality. An example of this is Higher Order Components (HOC) in React.
  • Factory Pattern: Instead of using a class constructor, use a static method that validates invariants.
  • Dependency Injection: Call methods from an interface so that it’s easy to swap implementations (e.g. a class calls a Notifier interface and can be passed an SMSNotifier or EmailNotifier if requirements can change).
  • Adapter Pattern: This is the same as an External Interface from Object Stereotypes in RDD. Adapters are classes that wrap calls to an external API to simplify a codebase.

Software Design Philosophy for Startups

I’ve heard advice that since startups are often teams with few people, teams don’t even have the time for abstractions. However, the 10-20% additional time it would take to think about generalizing code and creating clean abstractions results in at least 10-20% additional productivity (this was an idea from the book A Philosophy of Software Design) from readability and reliability improvements. I think that as long as too much time isn’t being spent on software design for small teams, the benefits outweigh the costs and startups should employ these principles of writing clean code:

  • Layered architectures for APIs like Domain Driven Design
  • DRY for generic frontend code like form validation between mobile apps and websites (although actual UI code can be duplicated; DRY UI code here can result in low performance)
  • Domain classes for keeping maintaining invariants local rather than existing in service methods
  • High awareness of unknown unknowns: documentation for things programmers should know should exist or code should be modified so that these are removed.