[placeholder]

Six Attributes of Beautiful Systems

At Squarespace, one of our most important values is:

DESIGN IS NOT A LUXURY

We work to bring excellence in design not only to the tools we create for our customers but also to the code and systems that power them.

On the surface, aesthetics and technology are not obviously related. The idea of beauty is not traditionally within the scope of software engineering which is typically concerned with algorithms, measurable elements such as time and space, performance, and abstractions. Yet, at the same time, engineers can be caught saying:

That’s a beautiful solution.

The essential difficulty is this: good design is plainly recognizable in objects that you can actually see and touch (such as art, cars, clothes, furniture, food, or buildings), but software is intangible and purely logical. In other fields, a creator can advance her craft because there exists a language for critique; a painting, for instance, is described in terms such as line, texture, movement, scale, or tone. Even the world of beer has a rich lexicon: full-bodied, malty, yeasty, hoppy, etc.

It’s unclear how software engineers are supposed to judge similar qualities in their work. Faced with this challenge, numerous questions have come to mind:

What does it mean for an abstract thing like software to be beautiful? We “know it when we see it” — but how does one actually go about creating it? Is it possible to isolate and articulate a set of properties that we can use to judge our work so we may critique and improve it? If so, what words would we use? Given that we care about good design at Squarespace, how do we marry aesthetics with code?

What Is a “Beautiful System”? Why Bother?

What I’ve learned at Squarespace is that technical aesthetics can be described precisely and that developing that vocabulary to use in conversations with colleagues about the merits of designs can lead to more beautiful systems, which carry significant practical benefits. Such systems are:

  • Easier to use, operate, maintain, and scale
  • Longer lasting and are more extensible far into the future
  • Powerful — solving more use cases with less complexity
  • Adaptable to new requirements
  • Simpler to test

Fred Brooks, author of The Mythical Man-Month, also wrote a remarkable book called the Design of Design. One chapter deals with what he calls “technical aesthetics” and sketches out ideas for how one could identify a logically beautiful system. In this post, I’d like to elaborate on this idea and show you six attributes of beautiful systems accompanied by specific, actionable tips, as well as examples, so you too can identify them in your own work. The same principles are applicable at every level, from the human interface all the way down to object design and method signatures. You should come away armed with a precise vocabulary for the aesthetics of software which you can share with your peers, leading to better and longer-lasting designs.

1 PARSIMONY

a.k.a. “Minimalism”

a.k.a. “Lean and Mean”

Parsimony is the ability to accomplish a great deal with few elements. It is marked by less conceptual clutter and fewer ways to do the same thing. Parsimonious systems are relatively more usable and learnable simply because there are fewer things to learn, fewer decisions to make, and fewer items to sift through to reach the things you need. They are also inherently safer, leaving fewer ways for you or other engineers to make mistakes in the future; fewer operations equals fewer unsafe operations.

In general, being aggressive in removing the extraneous is like scraping the barnacles off of a boat, which pays dividends in the long run in the form of less drag. In a parsimonious system, there are fewer moving parts to monitor, refactor, maintain, test, and to teach to other engineers who join the team. Such minimalism is probably the single biggest factor in determining how fast you can go. Remember: “Deleted code is debugged code.”

The Test: Ask Yourself...

For any construct in your system, ask yourself: is this actually just the combination of two other things? Is it redundant with an equivalent thing? If so, remove it!

Good – The CMYK color system is richly expressive with only four base components. Virtually any image can be rendered with these.

Bad – There are countless different types of screws on the market, many of which exist for very specific situations, and all require different tools for driving.

Example: PHP

PHP is an infamously non-parsimonious language. Numerous functions provided out-of-the-box exist for obscure use cases, such as:

  • ucfirst() converts the first character of a string to uppercase.
  • fgetcsv() gets a line from a file pointer and parses it for CSV fields. The same result can be achieved by simply composing two other existing functions: fgets() and str_getcsv().
  • join() and implode(), which are aliases of one another. One could be deleted.

A Word of Caution

Parsimony, like many of the other attributes in this post, is positive only up to a point, so beware of following this to the extreme. Excessive parsimony leads to the use of non-obvious idioms.

Example: A Non-Obvious Idiom in a TV Remote

For an Apple TV (4th Generation) remote, the same button that takes you to the home screen is also used for turning the device off. As a user, you must somehow possess knowledge of the fact that you just have to press and hold the button down to turn it off. This is a non-obvious idiom. If, as a designer, you introduce enough of these idioms, you create what’s commonly called a “learning curve.”

download-3.png

2 STRUCTURAL CLARITY

To have Structural Clarity, there must be a direct path from what you want to say to how you say it. This principle is largely focused on representation and reducing indirection.

Do most people prefer to type obscure commands into a command line interface just to launch a notepad app, or simply tap an icon with a finger? Do you prefer coding in assembly or in C? Do you prefer a list of GPS coordinates or a map with a drawn route? When the level of representation matches the level at which you are thinking about the problem, it can be said to be structurally clear.

To be clear, the concepts of your system have a structure to hang on and it must be plainly evident. A common way to achieve this is with familiar metaphors, such as the “Desktop” of Macintosh, the “spreadsheet” of VisiCalc, or the “blocks” of Squarespace.

Example: Two Ways to Model the Same Data

When designing data models, you are making structural decisions about representation. Which of these is clearer?

TransferRecordRefund
direction: -1
amountRefunded: "10.05"
amount: 1005
currency: "USD"
currencyID: 1
forCharge: “31f0alq8xz710a0”
parentTxID: "31f0alq8xz710a0"

Structurally, the design on the right will be clearer to others. There is less indirection than on the left, where several fields must be interpreted to have meaning (they must somehow know that direction -1 means a reversal, know that currencyIdx 1 corresponds to US dollars, etc.)

3 CONSISTENCY

Given familiarity with just part of a system, you can predict the rest of it. Note that Consistency does not mean that everything is the same. I’d call that “uniformity,” where if several elements were the same, then they’d be redundant and you’d follow Parsimony to eliminate them! Rather, Consistency requires there exists an underlying principle uniting all the elements and ensures some form of compatibility. The principle should be simple to state. For instance, you’d expect that all functions in a package try to maximize reuse and throw similarly formatted exceptions, use similar error codes, and take similar input types. It yields designs which are not only very usable and understandable, but also unlocks interoperability between elements.

Example: ISO 216 Paper Series

download.png
Good ExamplePrinciple
A Paper Series (ISO 216) Each sheet in the series is 2X the size of the previous sheet
Metric System Each unit is 10X bigger than the last
Vitsoe 606 Universal Shelving System All shelving modules have a standard width and are mountable anywhere on the tracks
UNIX pipes All inputs and outputs are plain text
Phoenician Alphabet All words are composed from 22 symbols, each has a sound

All of the above systems can be explained in a single sentence. With consistency, code is literally shorter , requires fewer tests , and is more resilient to change. When you introduce a new element to the system (say, a new feature module to the shelving system, or a new program in your UNIX toolbox, or even a new letter to the alphabet), you will not need to spend effort reworking other components in relation to it: a unifying principle ensures the newcomer easily “plays nice” with the rest.

Inconsistency, on the other hand, is incredibly expensive. If all your microservices outputted their logs in different formats, it’d be impossible to aggregate their outputs or easily search across them; engineers will need to learn the various eccentricities of each in order to debug issues. The cost to your organization due to inconsistency ends up being O(N) rather than O(1), where N is the number of things you’re maintaining. Allowing overhead to grow at that rate is unsustainable and eventually can be crippling to your development speed.

Bad Example: JavaScript Type Coercion

JavaScript type coercion is a famous example of inconsistency. Depending on what the arguments are, what operator you use, or even in what order you place the operands, the result might be a string, a number, or an object. To a developer, there exists no discernable reason or underlying principle to explain the erratic behavior.

Screen Shot 2018-07-14 at 1.53.58 PM.png

The Test: Ask Yourself...

Can I verbally describe my design with a broad, sweeping statement? Or is it riddled with holes, where I am forced to say things like:

ALL of our programs can {insert-behavior-here}! …
… except for {special case} whenever {special user} does a {bad thing} on a Monday…”

If you ever catch yourself using the word “except,” you have discovered an inconsistency. Find the exceptions and iron them out!

4 ORTHOGONALITY

Do not link what is independent. A change in one function should not affect another: each action changes just one thing without affecting others. In other words: there should be no side effects. Maintaining independence of functions avoids frustration by ensuring there are no “gotchas” to work around. At home, you’d normally expect that turning up the lights shouldn’t lower the thermostat, which shouldn’t influence your fridge temperature; it’d be maddening if every switch had some unexpected consequence on others. It’d also be frustrating if you opened the dishwasher door only to find that it blocked you from standing in front of the sink, making it impossible to easily rinse and load your dirty dishes. These types of problems make it really hard to get the environment “just right” or to complete routine tasks efficiently, so architects work hard to catch these situations and iron them out.

However, in almost all homes there is one prominent violation of orthogonality: faucet controls.

download.jpg

With many showers and faucets, as a user you want to manage two things: the water volume and the temperature. However, the controls are always frustratingly non-orthogonal, being instead composed of two knobs: “hot” and “cold.” They have side effects: if you want to make the water warmer, you turn the “hot” knob but doing so affects volume. If you like your temperature already but just want more pressure, you need to carefully turn both knobs just the right way. It is possible to do better. The safer and easier control style can be achieved via a thermostatic valve, which allows you to tune temperature and flow totally independently.

Similarly, if you take care to ensure that your features are orthogonal, they will be less tricky to operate and you will have fewer “gotchas” to hack around when you need to evolve the design. Code reuse dramatically improves as other engineers can simply take a component and reuse in a new place without worry. Clean orthogonality aids in upholding Parsimony as well: the more you can safely reuse, the fewer additional (and possibly redundant) features you need to add for new situations.

Additionally, the independence means you will be able to test components in isolation and have fewer cases to test. As a result, testability dramatically improves. In the opposite situation, where functionality is entangled, you will instead face O(N2) cases to consider, where N is the number of features that can impact one another.

5 PROPRIETY

a.k.a. The “It should JUST WORK” principle.

Do not introduce what is immaterial. Consider the important difference between the two types of complexities: incidental and inherent. Avoid exposing incidental complexity (also known as a “leaky abstraction”) where extraneous implementation details affect the interface. If you think about a stick-shift automobile in this light, you’ll see that the shifting of gears is incidental to the task of driving (managing a car’s speed and direction to get someplace) because the gearbox is just an implementation detail. Inherent complexities, on the other hand, are fundamental to the task and independent of underlying technology. Pumping gas (or plugging in) are incidental to driving; stepping on the accelerator is inherent.

To give another example of an egregious violation of propriety: initially, iPhones were unable to use FaceTime unless connected to WiFi. For most users, this limitation was completely arbitrary and frustrating: “Why should it matter what type of network I’m on, if I am connected to the Internet?!”

download-2.png

Proper systems minimize or eliminate exceptions like this.

Code Example

Here is an example of some code that demonstrates the difference between a Proper and an Improperly designed interface. Let’s try to book an appointment with Jim the Hair Stylist:

Throws an improper exceptionThrows a proper exception
bookAppointment(HairStylist person) throws JimIsVacationingInBermudaException bookAppointment(HairStylist person) throws TemporarilyUnavailableException

The improper example on the left gives too much information to the client and might even be revealing some personal facts about Jim. It’s also neither technically actionable nor relevant to the client that Jim is in Bermuda; all that they need to know is whether he is available or not. Further, it puts a small burden on the client to draw a conclusion: “If Jim is on vacation → he can’t cut my hair tomorrow → I should call again next week.”

In technical terms, impropriety forces client code to bear more responsibility, which leads to greater complexity and less flexibility if implementation details change: what if next year, Jim decides he would rather vacation in Bali instead?

The Test: Ask Yourself...

  • Is this “TMI” (Too Much Information)?
  • Does the recipient care? Is this actionable for them?
  • Is this inherent to the invoker or client’s task, or merely incidental to how you’re achieving it?

6 GENERALITY

Do not restrict what is inherent. Generality is the ability to use a function for many ends, where it is open to being reused and remixed again and again in powerful new ways. To achieve it, look at your designs and ask yourself whether there are non-essential constraints or particularities that can be stripped away to enable new use cases.

The Test: Ask Yourself...

What is the absolute minimum that this function needs to be able to assume about its inputs?

Example: Overly Constrained Code

Not General General
// Constrained to a narrow case
// of an apparel shop
Money getTaxesOwed(Shirt shirt)
// Generally applicable to
// anything taxable
Money getTaxesOwed(Taxable item)
sort(List<Number> items) // All that is required is
// that they be comparable
sort(List<Comparable> items)
// the “show” methods are
// constrained to specific types
image = LoadImage(“teapot.svg”);
ShowVectorImage(image);
ShowRasterImage(image); // fails
// Any image can be shown
image = LoadImage(filename);
showImage(image);

Example: UNIX Pipelines

$ cat report.csv | wc

UNIX Pipes are a powerful facility that enable a shell user to run all sorts of useful one-line commands by composing different programs together. With a simple syntax, several processes can be chained together by standard streams, with the output of one process feeding into the next. Since the expectations are standardized, there are no limitations on which programs can be put together.

Example: Legos

In the Lego system, any brick can link to any other brick without exception. No special features or eccentricities prevent any brick from being compatible with any other, which results in users being able to put together astounding creations without limitation other than their imaginations.

 By Kenny Louie from Vancouver, Canada (250/365 - Bricks) [CC BY 2.0 (https://creativecommons.org/licenses/by/2.0)], via Wikimedia Commons

By Kenny Louie from Vancouver, Canada (250/365 - Bricks) [CC BY 2.0 (https://creativecommons.org/licenses/by/2.0)], via Wikimedia Commons

In Conclusion

Although logical systems cannot be seen or touched, I hope you now see that software can be beautiful, and that it can be both critiqued and improved not just quantitatively but also aesthetically.

You’ve now seen six attributes that you can use to precisely describe your designs in future conversations with colleagues. In order to put these principles into practice, I suggest using a simple checklist of questions:

In my code / system / design...

  • Can the list of basic concepts be shorter?
  • Do any features have side effects?
  • Have I hidden away the incidental complexity?
  • Does this function make more assumptions about the input than totally necessary?
  • Do all parts relate to each other with a concise, unifying principle?
  • Does the design ever force users to use non-obvious idioms to express themselves?

I believe that if you keep these in mind, you will create beautiful systems which are not only useful but also a pleasure to use and to work on.

The Nuts and Bolts with Naz Hassan