Thoughts on Personalizing Software

I’ve been obsessed with productivity software for most of my life. I’ve sampled dozens, from simple personal tools (like Apple Notes) to full-featured productions complete with the kitchen sink (like Phabricator and JIRA). None of them ever felt comfortable — many were too simplistic, some were too prescriptive, and some were overwhelmingly customizable (although not necessarily in the ways that I would’ve liked).

The fundamental challenge is that software products are more-or-less centrally designed, and even assuming a product is well-designed with many use cases covered, software products are exactly as-is, no more or less. Without the ability to add specific functionality, change certain flows, or remove unnecessary complexity, most software ends up being prescriptive and less-than-ideal for most specific use-cases.

For example:

Google Calendar is a complex and extensively-designed product, but at some scale, any product will overlook or choose not to include certain use-cases that might be important to a subset of users. The end result is a product that’s not “just right” for anyone.

But does it have to be this way? Does software have to built towards a one-size-fits-all paradigm that exists in real-world products at the intersection of economics and the laws of physics?

To be fair, there are examples of customizable and extendible software. Some apps ship with extensive options. The proliferation of APIs provides a foundation for countless products built on the same underlying data (Nylas is a great example of an easy-to-use API over email and calendars, which normally depend on arcane protocols). And, developer experience aside, Salesforce Apex is a first step towards a world of personalized software.

Screen Shot 2018-05-20 at 12.17.04
Customization options for iTerm 2

I’d love to see a world of software that allows any user to change the way certain workflows work, add custom inputs, and automate steps that should happen after a particular trigger.

Technically, we’re starting to see the building blocks that would make this possible — serverless execution is getting faster, and GraphQL makes it easier to discover and understand APIs.

Practically, I think this would mean a shift in the way we think about software product design along two dimensions:

  • How extensive is the base product? At the minimalist extreme, the product is essentially an API to the underlying data, with minimal functionality out-of-the-box. At the kitchen-sink extreme, the product is likely complex, with built-in solutions for a lot of use cases, but may be harder to customize.
  • How much customizability gets exposed? It’s easy to follow the rabbit hole of maximizing optionality, leading to a product that lacks cohesion and becomes incredibly difficult to maintain or change.

Beyond that, there’s also the question of how customizations would be implemented. Perhaps writing software will eventually become a form of basic literacy, and everyone can plug in personal code into a product at customization points. Or perhaps a product figures out an effective visual programming environment for its domain, and customization becomes possible without having to “write code”.

The obstacles between prescriptive software and personalized software echo fundamental debates in software engineering and UX design. But personalized software could unlock a massive amount of value in knowledge work if every worker could leverage software’s scale and computation capabilities in a way that matches the way they work.

Photo by rawpixel on Unsplash

Tech Stacks are Overrated

In the process of interviewing dozens of junior and intermediate engineers, the questions candidates ask implicitly say as much about them as the rest of the interview. One question that comes up occasionally is some variation of “what tech stack are you using”? List some of the myriad Javascript libraries-du-jour and I get a murmur of approval; mention something mature and be met with silence or a disappointed “oh”. In fact, many outright say that they want to be working with the latest or “bleeding edge” technologies.

I get it, the “right” technologies are cool and shiny and have undeniable appeal. You feel invigorated when using them. For me, I’m excited about Elm, Crystal, and GraphQL.

But focusing on tech stacks and looking for the coolest technology during job interviews isn’t very valuable, and distracts from more valuable questions. Companies build software to reduce costs or capture value. Customers don’t care what tech stack companies are using, as long as they can get things done. A company using Node and bleeding-edge ES2018 doesn’t get to charge a coolness premium over a competitor using Rails; the shinier tech, by itself, doesn’t automatically create more value.

It can, however, increase costs. Mature technologies have a well-worn path to success: there are documentation or blog posts for everything you’d want to do, Stack Overflow questions for any issue you might run into, and a patch for every bug that might’ve existed in a v1. None of that might be true with the new and shiny, where any one of a dozen configuration options or plugins could break everything if you breathe the wrong way. There’s no clear path to a maintainable codebase and your ability to consistently create value in the long term.

Rather than ask “what’s your tech stack”, a more interesting question is why that tech stack makes sense for what’s being built. As a interview candidate, listen for a clear reasoning that makes sense — that’s a stronger signal of a company that’s more likely to be successful (and one where you can learn) than one that picked its technology based on what was cool at the time they started. To a good interviewer, that’s also a more impressive question.

It’s even more valuable to go beyond the technology and focus on the product and problem. What problem is the company trying to solve? How are they thinking about the problem, and what is their proposed solution? What kinds of problems do you want to solve? What kind of products do you want to build? As an interviewer, I’m looking for alignment between what we’re building and the problems and products you’re passionate about. As an interviewee, determine alignment around these questions first — and then you can ask about the technology, and whether that’s a reasonable choice for the problem. It’ll make for a much more interesting conversation for everyone.

If this makes sense to you, and you want to use technology to create value, I’m hiring a few engineers to empower every worker on Earth.

Photo by Jaz King on Unsplash

Service objects as test fixtures

In our Rails codebase, we often have tests that begin with many lines of setup code — declaring relevant variables, creating and updating models — to setup the database so we actually test what we intend.

Snippet 1
Most of this test’s body is setup

For background: we have Projects, which can have multiple Bids (each of which is associated with a different user — in other words, users can submit a bid to a project). The project’s creator can “accept” a bid by offering the bidder a Contract.

Tests with lots of manual model setup causes at least three problems:

  1. Tests become harder to read, because the setup code isn’t logically important to the test. You end up having to skim through a lot of code clutter to get to the important test code.
  2. Tests have to know exactly how models fit together, and if that changes, all the corresponding tests have to change as well. For example, in the example above, you’d have to know that bid models and contract models are both associated with a project (they’re not valid unless you specify a related project), and the status of both the bid and contract have to be as specified (otherwise you’ll end up with an invalid state error). Easy enough if you just wrote the underlying code, but impossible to keep in mind for someone new to the code (which could be you, a few weeks later).
  3. Sometimes you have user-facing concepts that are implemented as a “derived” state of data models. For example, in addition to the concepts above, our UI has the concept of “direct offers”, which is implemented as a particular combination of bid state and contract state. This leads to a logical disconnect when we want to test some aspect of that functionality, but the setup code doesn’t say anything about a “direct offer”.

For unrelated reasons, we started moving business logic functionality into service objects, especially when there are side effects that need to happen in certain cases. The thinking behind this is a subject for a different post, but the end result is that (for example) we can create a bid for a user on a project by simply calling BidOnProject.new(user, project).create, which takes care of creating the Bid instance, updating statuses, setting prices, and creating and sending notifications. Creating a direct offer is similarly simple: BidOnProject.new(user, project).create(direct_offer: true)

Lately, I’ve found myself using these service objects to setup test state as well, with code that roughly looks like this:

Snippet 2
Simplified setup via a service object

Much less setup, much more readable test code. Since this runs the same code as “normal” app, tests don’t have to know the details of how models and state fit together, and changes to that only need to happen in one place. Using service objects is better than using custom factories for this reason as well — why duplicate the business logic? Finally, to the extent that you have service objects for user-facing concepts, tests become more coherent and clear, which ultimately make them more reliable and valuable.

 

There’s More to Coding than Just Writing Code

For most of my life I firmly believed that a Computer Science degree wasn’t necessary to work as a software engineer; there exists a massive amount of resources and practice opportunities to learn outside of the context and cost of a university degree. I maintained this perspective even after I ended up getting a CS degree, partly because of the lack of practical skills in a typical CS program.

I still don’t think a CS degree is a requirement, but more recently I’ve realized that it’s a very good proxy for a baseline of skills and ways of thinking. If nothing else, a CS degree indicates that someone has spent a lot of time understanding software patterns, becoming familiar with a range of patterns, and developing the basic abstractions necessary to understand more complex problems.

This post is based on observations from my personal life and interviewing junior and mid-level engineers for work. I taught myself to code in high school from a few books, got a CS degree along the way, and continue to refine my software thinking through higher-level books and videos. I work at Trustwork, a seed-stage startup in San Francisco with four engineers. For the near future, we’ve made the choice to prioritize shipping speed over harnesses and processes to train very junior engineers; at our size we don’t believe it’s possible to be great at both. This perspective underlies this post. This post is not intended to be specific to any technology, language, or library. This post doesn’t split semantic hairs — I use the terms code and software, developer and engineer, etc interchangeably. 

Understanding Software

At a first approximation, learning to code simply means learning to write code — the act of manual code generation. The much more valuable component is learning to think about software. In my experience, the latter is almost completely overlooked by existing resources.

The former approach conflates the skill of coding with the act of typing code. This approach generally consists of a rough project description, code listings, and descriptions of what the code literally does. The teaching approach is essentially “write this code, in the right place, and you’ll have a working project, which means you’ll understand what we did”. Success comes from carefully following instructions, and understanding rarely happens. The outcome for going through this form of training is someone who is very good at rote syntax pattern matching, someone who is good at putting network calls in the relevant framework-provided method and filling in boilerplate.

Admittedly, a lot of software is little more than boilerplate, but that’s a different topic.

Pattern-matching on syntax breaks down as soon as the syntax or framework looks unfamiliar. Instead, being able to pattern-match concepts and abstractions is much more valuable. For example, both snippets below represent a network request, even though they look very different.

Put differently, concepts and abstractions are the vernacular of software. As with human languages, an intuitive understanding of constructs and slang are a core part of being a native speaker.

4b2
Source

Building Software

Syntax pattern-matching facilitates building up: writing software from a foundation to successively higher levels of abstraction. For each layer, the developer can take advantage of an intuitive understanding of the layer below because she just wrote it. She understands the abstraction, and can therefore make progress.

However, this doesn’t work if she’s asked to build down: plugging code into existing use cases or implementing an interface. During our interviews, we test this by showing candidates a code snippet and describing its intended behavior, and asking them to implement the API being used to make the code snippet work. Candidates who do well here are able to identify the functional concepts in the code snippet, as well as the potential seams in the stack of abstractions where they can insert their implementations. In other words, they’re comfortable being dropped in the middle of a stack of abstractions and building both up and down.

Thinking about Software

Beyond building software is building great software. There are many ways to define great code (elegant, clear, performant, well-documented, etc), but producing any kind of great software requires thinking about the code both granularly (perhaps in picking the best name for a function) and abstractly (perhaps when refactoring for readability or better performance).

Across the spectrum, this can only be done with a deep, intuitive understanding of both the application code at hand, and the breadth of existing software implementations and developer expectations.

A deep, intuitive understanding is characterized by simplicity — the hallmark of such an understanding is the ability to explain the subject in one short, clear sentence. It comes from wallowing in the complexity, understanding the subject one line or short chunk at a time, pattern-matching ideas and abstractions, until the whole subject becomes something you can hold in mind all at once. You can inspect the idea from different angles, zoom in to confirm details, and use it as a building block to understand a bigger subject. Some would call it grokking the subject.

Bonus: CS as a Common Language

Credit to David Ko for this line of thought

Practicality aside, the traditional CS topics form a common foundation and vocabulary when talking about software.

For data structures, a deep, intuitive understanding of how strings, arrays, hashes, and sets covers 99% of the code we write. A “deep, intuitive understanding” might be defined as being able to implement each of those data structures and some of their common functionality using the primitives of a language like C (where there are no existing standard library to hide behind).

For algorithms, I think being able to evaluate performance is more important to knowing how to implement any of the traditional ones. This means understanding what Big-O represents, and, crucially, choosing the relevant N. This also means knowing common latencies to within an order of magnitude, and being able to identify bottlenecks in an implementation.


👉 If this resonates with you, we’re hiring and would love to hear from you. Details here.

Photo credit: Fabian Grohs on Unsplash

Behavioral Observations from Taking Over SXSW

I just got back from taking over SXSW with Trustwork. We ran a scavenger hunt with multiple stations giving out free stuff in exchange for user signups. This post is the second of two posts on learnings and observations from that week. Part 1 is here.

My intent is to present some personal observations. It is not to disparage anyone or any usage pattern. I’m also not saying that Trustwork had a perfect signup flow — for example, we definitely could’ve had more performant code, and we had a contentious Confirm Password field.

Phone Usage

In my own life, I’ve been very conscious of how I interact with technology. I’ve cut off apps when I don’t like the interactions they’re inducing. As a result, it was a reality check for me to observe how most people interact with phones — a lot of behavior seemed instinctive, but laborious. Percentages are precise to within 10%, based on my own observations across a sampling of several hundred pedestrians in Austin:

  • When presented with an input field (for phone number, in this case), 80% of people instinctively started filling it out without prompting.
  • When presented with an entire form (name, email, password), 60% of people instinctively started filling it out without prompting. An additional 10–20% of people did so after a nod confirming that they should fill it out, before they knew where the information was going.
  • The vast majority of people (80+%) didn’t use autocomplete, instead filling out every field manually.
  • The vast majority of people (90+%) didn’t have any autosuggest-password feature enabled or didn’t use it if available. Based on the lack of thinking time when filling out the form, it appears that almost everyone reuses an existing password.
  • 30–40% of people mistype their password between the Password and Confirm Password fields. That stat could be interpreted either in favor of or against having a Confirm Password field.
  • The vast majority of people (80+%) didn’t use a password manager (this includes people who decline the browser’s dialog asking to save the password). Notably, one person manually entered his password into his 1Password app after signing up.
  • 30% of people had trouble getting around their phones. For example, we send an SMS verifying phone numbers. People had no trouble tapping on the notification to get to messages, but if asked to go back to the message after they left that app, some had difficulty getting back to it. Some people also had trouble finding their web browser.
  • 40–50% of the people who were experiencing very slow load times quit background apps in an attempt to make the site load faster. Doing so didn’t help.
  • 10% of people didn’t know their own phone number and had to look it up in the Phone app or from a friend’s phone

Human Behavior

Across hundreds of people, some behavior patterns become apparent. All of the above qualifiers apply here — this information is presented neutrally, without judgment; percentages are precise to within 10% and based on my own observations.

  • To “Free shirt?”, “Free cupcake?”, etc, responses were mostly binary — 90% of people either wanted the thing and stopped, or didn’t. Very few became convinced after some more talking.
  • Practically 100% of euphemistic Maybes (“I’ll come back later”, “On the way back”, “Let me grab my husband [who had gone ahead]”) were effectively Nos; they never came back.
  • 30% of people didn’t ask what Trustwork is before they finished signing up, or at all.
  • About half of the people who did ask what Trustwork is did so in the middle of the signup flow, usually after they already verified their phone number.
  • Most people who did ask about Trustwork were looking simply for some answer that seemed to make sense, rather than any genuine curiosity. As a result, it was possible to calibrate a couple of answers of varying complexity depending on the listener — my longest pitch took about 20 seconds; the shortest took three.
  • With groups of 2–3 (sometimes 4), it only takes one person to say Yes; the rest of the group will wait. Larger groups required more people to say Yes. If one person in a group says Yes, 80% of the time, the rest of the group will wait; occasionally they’ll convince that person to move along. About half of the time, the waiting group members end up saying Yes (usually because they were already on the fence; the fact that they were already spending time waiting was not an effective point).
  • For male-and-female couples, women led the interaction during the day, while men led the interaction at night.
  • Conversion rate, in rank order:
    • Free pizza was easiest.
    • Free cupcakes and cookies are easy, but it was difficult to manage “loss” — people who grabbed an item in passing, or who grabbed multiple items.
    • Free limo rides are mixed — some people are very skeptical, whereas other people jump at the opportunity.
    • Free shirts are somewhat easy. Availability of different sizes complicated things slightly, although most people were willing to accept sizes (S/M/L/XL) one step away from their stated size.
    • Free drawstring bags are moderately difficult.
    • Free photos were very difficult, although we got better results when it was positioned as the hardest-to-find item on the scavenger hunt.