On specialism vs. generalism

· 5 minute read

“You’re basically not going to be an iOS engineer anymore?”

When my good friend Soroush asked this upon hearing that I had taken a new job at Stripe, I doubt he thought very much about how it’d be received. It really threw me for a loop, however. I didn’t exactly consider myself to be undergoing a career change, but was I? It’s true that I’m not going to be developing for iOS in my new role, but I hadn’t always worked in this capacity at previous jobs either. Did spending the better part of five years focused on iOS make me an “iOS engineer”? If so, when exactly did I become one and when did I subsequently cease to be? Should this kind of designation be descriptive, based on one’s actual day-to-day, or prescriptive, aspirationally describing the work being primarily sought out and anticipated?

Work as a software engineer for long enough and it’s highly likely that you’ll end up having a say over whether or not you go deep on any particular sub-discipline (and if so, which), or choose to primarily float around the surface, swimming back and forth and maybe holding your breath to go under for a bit here and there, but not taking many dives necessitating special training or equipment.

There’s no right answer here, and it’s really not a strict dichotomy anyway.

While programmers can undeniably be either specialists or generalists, there’s a whole lot of grey in the middle. As opposed to inherently being a specialist, it’s also very common to specialize over a period of time. Perhaps this is a subtle difference, but I think it’s one worth teasing apart; one can act in a specialist capacity when the situation dictates - and I presume that effectively every “generalist” does, from time to time - without self-identifying as such for the long haul.

There isn’t a right answer because one isn’t better than the other, but also because many teams should contain both specialists and generalists in order to perform their best work. The best products are often brought to fruition through a combination of generalist thinking and specialist expertise. Only specialists have the domain knowledge necessary to build best-in-breed software that takes full advantage of the platform being built for; given how advanced the various platforms that we build for in 2019 have gotten, it’d be near impossible to sweat all the right details without having first dedicated yourself to fundamentally understanding a particular one’s intricacies. We’re all very lucky that many have.

At the same time, specialists run the risk of “only having a hammer,” and as such, having every possible project “look like a nail.” With only one tool in your belt - a deep but relatively narrow area of expertise - it’s easy to inadvertently build an app that really should’ve been a website or vice versa. Or to have a great idea that you can’t quite realize, despite your excitement, due to it requiring both frontend and backend work. Said idea might be exactly the provocation that can prompt one who has historically specialized to start branching out a bit. But after having done so, are they still “a frontend developer” or “a backend developer”? Clearly, such labels start to lose their significance as we tear down the boundaries defining what we’re able to do, and perhaps more importantly, what we’re interested in doing.

In the Twitter, Slack, and GitHub circles that modern software developers often travel in, it’s easy for a discrepancy to form between how one is best known vs. how they actually view themselves. Tumblr was quite popular during the time that I led iOS development there, which gave me the opportunity to write and speak about the work that we were doing, and even release some of it as open source. These slide decks and blog posts neglected to mention that I was actually hired to be a web developer and only moved over to iOS as needs arose, subsequently parking myself there for a few years to come. I built Rails backends and React frontends at my next job, but at an early-stage company with a much smaller platform, where we primarily worked heads-down without much outward-facing evangelism for our technology. Few knew.

I’m not unique in this regard. One of the best mobile developers from my time at Tumblr has since switched over to the web. Another, an expert in animations, gestures, and UI performance, is now a designer. Since acting as a specialist at a high-profile company can cement your status as such well after you’ve stopped working in that capacity, it’s crucial not to let outside perception prevent you from shaping your career however you see fit.

In August 2014, I gave a talk entitled Don’t be “an Objective-C” or “a Swift Developer” to a room full of new programmers who were learning how to build iOS applications at the Flatiron School. The Swift programming language had been unveiled only two months prior, and reactions amongst iOS developers were divisive, to say the least. Many felt as though it was finally time for a modern language to replace Objective-C, and that such a change was long overdue, while others didn’t believe that Objective-C needed fixing, and would’ve preferred if Apple’s resources and the focus of its community were directed elsewhere. My goal was to try and convince these new engineers that they shouldn’t aspire to land in one camp or the other, but rather to learn the underlying, transferrable programming concepts, and to expose themselves to many different ways of concretely building software. Without understanding what’s out there, how can one make an informed decision as to how they should spend their time? Even if you decide to put down roots in a single community, how can you avoid perceiving the way that that community has historically operated as being the way that it should be going forward?

I feel like I could give this same talk today, to a more experienced set of engineers no less, and simply replace “Objective-C and Swift” with “frontend and backend” or “mobile and web.” The idea is the same - technologies move fast and careers are long, and while you may enjoy being a specialist or a generalist for some time, you never really know when your situation could change and when circumstances may warrant otherwise. Or, when you might simply feel like trying something new.

When I write Ruby, it’s painfully obvious to me that I don’t know Ruby to nearly the same extent that I know Swift. On some days, this makes me sad, but it just as often makes me feel empowered. Perhaps I’ll decide to spend the time needed to achieve Ruby mastery, or maybe I’ll end up retreating back to Swift at some point in the future. Or, more realistically, I’ll get slightly better at the former and slightly worse at the latter and come to peace with that, just in time to shift towards learning something different altogether. In any case, how others describe what I do, and more importantly, how I view it myself, remains a fluid work in progress.

I don’t expect this to change, and this I am at peace with.


Originally published on Better Programming.

Software without coding, documents vs. apps, or the impossible dream of the “personal CRM”

· 5 minute read

A couple of years ago, I asked for a recommendation for a “personal CRM” – a client-relationship management tool intended for individual use. As it turns out, I wasn’t the only one asking, and a solution still hasn’t emerged despite outsized interest from the very designers and developers who should be well-poised to create it. Why?

I’ve come to believe that this is because “personal CRM” means something a bit different to each person who asks for it. To my friend Shane, it’d be akin to a proactive virtual assistant, skewing towards the “magical” end of the spectrum. I myself have envisioned a few different products, all which I’ve lazily described using this umbrella term: one for letting me know when I haven’t seen a certain friend in a while, another for applying “tags” to former coworkers, etc. During my current job search, I’ve needed different tools at different times: one centered around colleague outreach to start, but another for tracking interview processes as the’ve continued to progress.

When faced with a moving target and only concerned with meeting one’s own individual needs, we don’t need an “app” or a “product” per se. A document can be structured in a way that meets today’s needs, with the flexibility to easily evolve as requirements change. My “personal CRM” is currently a spreadsheet – I could’ve built a version using Excel or Google Sheets, but I chose Airtable because it stores data in a more structured, database-like format. This lets me build more powerful, customized views on top of my data.

A database. With custom UI sitting on top of it.

This is “an app,” my friends.

Documents vs. apps

From this perspective, Airtable isn’t necessarily a product in-and-of-itself, but a platform that empowers you to build your own products – and it’s far from the only one. Coda, Notion, Quip, Clay, and Actiondesk are also similar in that they’re democratizing software development under the familiar guise of “collaborating on documents.1” But their document-sheen is only skin-deep:

A creative palette of app-like functionality that you can mix and match

Build something as unique as your team

We took the quintessential parts of apps and turned them into building blocks for your docs

Our goal is to make it much easier to build software

Forget the back and forth with your tech teams. Build powerful automations yourself

Zapier, IFTTT, and Apple’s Shortcuts focus more on integrations and less on data storage and UI, but they’re also key parts of this emerging space – not all software needs a visual user interface2. Even Slack – an enterprise chat application on the face of it, has ingrained itself in no small part due to its integrations platform, allowing teams to build mini-products and workflows that suit their own specific needs.

These aren’t necessarily new ideas – Excel macros and Google Sheets scripts3 are less holistic solutions that nonetheless strive to answer the same question: how can we lower the software development barrier to entry?

Apps for small audiences

Part of what made my own CRM so easy to build was the assumption that I’m going to be the only one who ever uses it. Turning it into a consumer-facing product would be a much taller task, but there’s still a ton of value to be captured by software that’s only used by individuals or teams internal to a company.

Shishir Mehrotra, the co-founder and CEO of Coda, highlighted this very point in a piece titled What Will Software Look Like Once Anyone Can Create It?:

We’ll start designing apps for small audiences, not big. Companies will run on their own apps, hundreds of them, tailor-made for every team, project, and meeting. In this world, there’ll be no such thing as an edge case. All the previously underserved teams and individuals will get a perfect-fitting solution without needing to beg an engineer.

Internal applications also have lower user experience expectations than their consumer-facing counterparts, and are simpler for users to authenticate with. And by providing value, they don’t require business models of their own to justify the upfront development cost.

That doesn’t mean that these hurdles can’t be overcome, however – we may be building some consumer applications without code sooner than we know it. A product like Squarespace could shift ever-so-slightly towards app development, or Google’s Firebase could aim for broader consumer appeal. Bubble is one platform purporting to already facilitate this sort of codeless development experience today. At least in some cases, it does seem to be working.

Another approach: instead of using one app-building “platform”, what if the makers of the future study the skills necessary to combine existing applications together themselves, instead of studying computer science fundamentals? MakerPad is one such example of a program and community attempting to educate in this regard:

While such an approach may not suffice in perpetuity, it could mean the difference between bootstrapping yourself to profitability and prematurely taking venture capital in order to hire an engineering team.

What happens to software developers?

As a software developer, should I find this concerning? I admittedly do not, and think that Steven Sinofsky succinctly articulates why by providing some key historical context:

The layers of abstraction that software developers work on top of are continually changing, but there will always be problems to solve despite moving higher and higher up the stack. Most of the platforms covered in this essay already integrate with the popular services of the moment, but developers will always have an advantage insofar as being able to build their own integrations to augment whatever a platform vendor happens to provide out-of-the-box. Understanding databases makes it easier to create a complex Airtable sheet, just as being familiar with loops and conditionals makes you better equipped to craft a complicated workflow in the Shortcuts app. The line between what is and isn’t “programming” really starts to blur.

Similarly, there has always been a gradient between what can be done without code and when you’ll eventually hit a wall with that kind of approach. Adobe Dreamweaver never quite obviated writing your own custom HTML, did it? I don’t personally foresee this ceasing to be the case.

But traditional software development being long for this world isn’t an excuse not to keep up with the changing times. Adobe’s new XD/Airtable integration might be marketed as a prototyping tool today, but how long will it be until those prototypes are good enough for a single designer to ship as production software?

If you don’t like change, you’re going to like irrelevance even less.

  1. Coda goes so far as to intelligently translate traditional desktop documents into common mobile app paradigms. A section in a document becomes a tab in an app’s navigation, and a select box with multiple states can be edited using a native swipe-gesture. 

  2. Especially as voice control and home automation continue to increase in prominence. 

  3. Not to mention, entire Google Sheets apps

GraphQL and the end of shared client-side model objects

· 7 minute read

In traditional client-server development, client-side models don’t often differ too dramatically from server-side models.

Should they?

A standard RESTful API might serialize a server-side user model as such:

{
  "id": 12345,
  "firstName": "Bryan",
  "lastName": "Irace",
  "avatar": {
    "thumbnail": "https://some/s3/url/a39d39fk",
    "large": "https://some/s3/url/39fka39d"
  },
  "profession": "Software developer",
  "location": {
    "city": "Brooklyn",
    "state": "NY"
  },
  "friendCount": 40
}

Instances of this same user model type can be vended by multiple routes, e.g.:

  1. /me – A route that returns the currently authenticated user
  2. /friends - A route that returns the current user’s friends

RESTful APIs aren’t inherently type-safe, so a frontend developer will generally learn that these routes both return objects of the same User type by looking at the API documentation (and hoping that it’s accurate1), or by eyeballing the HTTP traffic itself.

After realizing this, a type definition like the following can be manually added to your client-side application, instances of which you can populate when parsing response bodies from either of these two routes:

interface Avatar {
  thumbnail: string;
  large: string;
}

interface User {
  id: number;
  firstName: string;
  lastName: string;
  avatar: Avatar;
  profession: string;
}

This shared, canonical User model can be used by any part of the frontend application that needs any subset of a user’s attributes. You can easily cache these User instances in your client-side key-value store or relational database.

Suppose that your application includes the following capabilities (and is continuing to grow in complexity):

  1. Rendering user profiles (requires all user properties)
  2. Viewing a list of friends (requires user names and avatars only)
  3. Showing the current user’s avatar in the navigation (requires avatar only)

Your server will initially return the same user payload from all of these features’ routes, but this won’t scale particularly well. A model with a large number of properties2 will be necessary to render a full profile, but problematic when rendering a long list of users’ names and avatars. It’s unnecessary at best and a performance bottleneck at worst3 to serialize a full user when most of its properties are simply going to be ignored.

Perhaps your API developer changes your server to return only a subset of user properties from the /friends route. This is followed by a change to the API documentation and a hope that your frontend engineer notices, at which point they’ll add a new type to the client-side codebase. Perhaps this new type looks something like:

interface SimpleUser {
  id: number;
  firstName: string;
  lastName: string;
  avatar: Avatar;
}

At this point, your frontend will need to:

  1. Keep track of which routes vend User instances vs. SimpleUser instances, when processing HTTP responses
  2. Have its caching logic updated to support both of these different types

User vs. SimpleUser is admittedly a coarse and superficial distinction. If we add a third flavor to the mix, what would we reasonably name it?

Instead of SimpleUser, we could instead call this new type FriendListUser, named after the feature that it powers. Having separate user models for each use case is a more scalable approach – we could end up with quite a few different versions, whose names all accurately convey intention better than “simple” does:

  • FriendListUser
  • EditAccountUser
  • ProfileUser
  • LoggedOutProfileUser

The risk here is that we’re likely to incur a lot of overhead in terms of keeping track of which routes vend which models, and how to make sense of all of these different variants when modeling our frontend persistence layer.

Reducing this overhead by more tightly coupling our client-side type definitions to our API specification would be a big step in the right direction. GraphQL is one tool for facilitating exactly this.

GraphQL

There’s a lot to like about GraphQL – if you’re looking for a comprehensive overview, I’d recommend checking out the official documentation.

One advantage over traditional RESTful interfaces is that GraphQL servers vend strongly-typed schemas. These schemas can be programmatically introspected, making your APIs self-documenting by default4. But this is an essay about client-side models, not avoiding stale documentation.

With higher model specificity comes higher clarity and efficiency, the primary downside being the additional work involved to maintain a larger number of models. Let’s dig deeper into how code generation can mitigate this downside.

By introspecting both:

  1. Our backend API’s strongly-typed schema
  2. Our frontend app’s data needs

We can easily generate bespoke client-side models for each individual use case.

First, we must understand how GraphQL queries work. In a traditional RESTful API server, the same routes always vend the same models. Let’s say that our GraphQL server exposes the following two queries:

  1. me: User
  2. friends: [User]

While both queries expose the same server-side User model, the client specifies the subset of properties that it’s interested in, and only these properties are returned. Our frontend might make the following query:

me {
  firstName
  lastName
  location {
    city
    state
  }
  avatar {
    large
  }
}

The server will only return the properties specified above, even though the server-side user model contains far more properties than were actually requested.

Similarly, this query will return a different subset:

friends {
  firstName
  lastName
  avatar {
    thumbnail
  }
}

Code generation tools can introspect these client-side queries, plus the API schema definition, in order to:

  1. Ensure that only valid properties are being queried for (even directly within your IDE, validating your API calls at compile-time)
  2. Generate client-side models specific to each distinct query

In this case, the generated models would look as follows:

// Generated to support our `me` query

interface User_me {
  firstName: string;
  lastName: string;
  location: User_me_location;
  avatar: User_me_avatar;
}

interface User_me_location {
  city: string;
  state: string;
}

interface User_me_avatar {
  large: string;
}

// Generated to support our `friends` query

interface User_friends {
  firstName: string;
  lastName: string;
  avatar: User_friends_avatar;
}

interface User_friends_avatar {
  thumbnail: string;
}

Each of our app’s components can now be supplied with a model perfectly suited to their needs, without the overhead of maintaining all of these type variations ourselves.

Trees of components, trees of queries

User interface libraries like React and UIKit allow encapsulated components to be composed together into a complex hierarchy. Each component has its own state requirements that the other components ideally needn’t concern themselves with.

This is at odds with traditional RESTful API development, where a single route will often return a large swath of data used to populate whole branch of the component tree, rather than just an individual node.

GraphQL query fragments better facilitate the colocation of components and their data requirements:

const FriendListItem = gql`
  fragment FriendListItem on User {
    firstName
    lastName
    avatar {
      thumbnail
    }
  }
`;

const FriendListRow = props => {
  return (
    <Container>
      <Avatar source={props.user.thumbnail} />

      <NameLabel firstName={props.user.firstName}
        lastName={props.user.lastName} />
    </Container>
  );
};

This results in a “query hierarchy” that much better aligns with our component hierarchy.

Just as a UI rendering layer will walk the component tree in order to lay out our full interface hierarchy, a GraphQL networking layer will aggregate queries and fragments into a single, consolidated payload to be requested from our server.

Heterogenous caching made simple

GraphQL is a high-level query language; while you can use it to query a GraphQL server, client-side libraries such as Apollo and Relay can act as abstraction layers on top of both the network as well as an optional local cache5.

(Additionally, Apollo and Relay both also handle the code generation and query fragment unification outlined in the sections above 💫)

Traditional client-server applications often end up with logic that looks as follows:

// Check if we have a certain user in our cache
if let user = db.query("SELECT FROM users WHERE id = \(id)").first {
  callback(user)
}
else {
  // Fetch from the network instead
  API.request("/users?id=\(id)").onSuccess { response in
    callback(response.user)
  }
}

In this case, we’re querying for the same user via two different mechanisms: SQL against our local database and a URL-encoded query string against our remote server.

Apollo and similar libraries allow us to more declaratively specify the data that we need in one unified way. This level of abstraction lets us delegate the heavy-lifting – checking whether our request can be fulfilled purely from cache, and augmenting with additional remote data if not.

To continue our example: if you first made a friends query, your cached users would only contain firstName, lastName, and avatar.thumbnail properties. A subsequent me query for one of those same users would hit the server in order to “fill in” the additional properties – location and avatar.large. From this point forward, subsequent friends or me queries could avoid the network roundtrip altogether5.

As long as two user models have the same unique identifier, it doesn’t matter which subset of their properties were fetched in which order. Apollo will take care of normalizing them for us.

Sound magical? It certainly can be, for better or for worse. Like all high levels of abstraction, it’s amazing when it’s working and infuriating when it isn’t.

But such is the promise of GraphQL; the sky is the limit for tooling when a typed schema definition is the foundation being built upon. Tooling of this nature can make a premise that would’ve otherwise seemed prohibitively unwieldy – having a distinct client-side model type for every slight use case variation – not only achievable, but ideal.

  1. More likely if it’s generated using something like Swagger, less likely if your API engineer is manually doing their best to keep it up to date. 

  2. Just imagine how many properties a Facebook user is comprised of, for example. 

  3. Not to mention, disrespectful of your users’ time and cellular data plans. 

  4. Tools like GraphiQL allow you to see exactly which server-side models are vended by each of your GraphQL queries. 

  5. Depending on your caching policy, of course.  2