Category Archives: Software Development

How Senior Software Developers Think

Senior Developers are expected to be more skilled in the technical aspects of software development. Just by having more years of experience they should be able to solve more problems, have more techniques, be faster, etc.

But, this alone is not enough to make them “senior” in my view. They will be more valuable than a junior developer doing tasks, but you can’t scale a team with a group of expert task doers.

The biggest differentiator between a junior developer and a more senior one is in the scope of their thinking and planning.

To keep it simple, imagine four levels of engineer (Jr, Dev, Sr, Lead). Here are some of the differences aside from programming skill. This distinction has nothing to do with people management—none of these levels have management responsibilities.

  1. Jr: Right out of school. Cannot do any task without some help. Can plan a couple of days of work.
  2. Dev: Can do most tasks independently. Can plan projects that take weeks to do.
  3. Sr: Takes product specs and writes functional/implementation specs and plans. Can plan projects that take months to do. Can coordinate the work of a team.
  4. Lead: Can plan projects that may take years. Thinks about overall architecture. Establishes processes. Can coordinate the work of multiple teams.

There is more to it than this, but the essence is increasing scope of time, planning, and coordination.

Another way to think of it is in what their goals are:

  1. Jr: Complete the ticket: e.g. Fix the bug, deploy the build
  2. Dev: Complete the project: e.g. Implement the export to CSV feature as specified
  3. Sr: Achieve the mission of the project/team: e.g. Increase paid conversion, reduce the crash rate
  4. Lead: Achieve the mission of the company: e.g. Increase profit margin by 10%, enter an adjacent market

To be fair, everyone should be working on the mission of the company. The difference is how they think about the work they are doing and how they evaluate success.

Construct PRs to Make Reviewing Easy

On my first day at Trello, my peer mentor showed me two things. One of them ended up being the topic of my talk on Reactive Programming. The second thing he told me was, on our team, PR’s should tell a story.

Then we pair constructed a PR. To be clear, he was already in a local branch, and the code was already written and committed.

He showed me how he used git reset and git rebase to rewrite the commits. He explained that he was rebuilding the commits to make the PR easy to review. He constructed each commit to be atomic—to do a single coherent thing.

This was the PR style for our team for the whole time I worked there. The goal of a PR was to make it easy to review, and we expected reviewers to open each commit in a tab and review them one-by-one. We considered it acceptable to decline a PR if it was too big or hard to review (in practice, this happened very infrequently).

Other practices:

  1. PR’s were mostly less than five commits. If they were more, it was because each commit was tiny.
  2. Commits were tiny because we wanted them to be atomic—only one idea. For example, if I saw a spelling error in a comment, it would be its own commit. If I renamed a function, the rename refactor would be its own commit.
  3. Most PRs were merged within hours of being opened. Many even sooner.
  4. This is true even though we required 2 reviewers per PR.
  5. It only took 10-20 minutes to review a PR
  6. If PRs were big, we’d do a “Live review” over zoom to go back and forth quickly
  7. Single Jira tickets that required a lot of code were PR’d in parts.
  8. Almost every review comment was pointing out a blocking defect (or possible ones), not nitpicks.
  9. If the code changed the UI, we’d attach before and after screenshots.

These practices helped us, but the important part is our team mindset that PRs were constructed to be easy to review.

New Versions Should be Substitutable

The Liskov Substitution Principle is an Object-Oriented class design guideline that says that a sub-class should be substitutable for a super-class and that clients to the super-class should not be impacted.

You achieve this by specifying a contract for each public method of a class.

First, you specify a pre-condition/requirement.

If the caller meets the requirements, then the method makes a promise of a post-condition and result.

A sub-class is substitutable if it makes the same promises under the same requirements. It is also substitutable if it can make the same promise with fewer requirements. And, finally, it’s still substitutable if it makes more promises.

So, sub-classes can require less and promise more than the specification of their super-classes. But, they can neither require more nor promise less.

Some OO languages, like Eiffel, have ways of encoding the contract, but in Swift and many other languages, this is something usually stated in documentation.

But, substitution is not just an OO principle, or at least, I don’t only apply it to super/sub class relationships. I think it should be applied to versioning of any public library of any kind of code (OO, functional, whatever). To do this, think of vNext as a subclass of vCurrent.

So, vNext can require less and promise more. It can’t:

  1. Add new required parameters to a method
  2. Add exceptions to methods that didn’t have them before
  3. Change the name of a method
  4. Change the return type unless the new type is substitutable
  5. Change parameter types unless the old type is substitutable
  6. Add new required methods to a prototype

Unfortunately, many languages would think of some acceptable changes as a compile-time breaking change. For example, in Swift if a method returned T? and you changed it to return T, then callers that used guards would not compile. Strictly speaking, T is always substitutable for T?, but you should not do this change.

This is not how many library authors think, Instead, I think we’ve been corrupted by semantic versioning and the idea of acceptable breaking changes. Under substitutable versioning, there is no such thing as a breaking change.

Under semantic versioning (major.minor.patch), most library providers only think of patch version increases as being substitutable and for sure major version increases are not. I would say in practice, minor version increases are sometimes breaking.

In a fully substitutable versioning system, this would never be the case. vNext would always be substitutable for vCurrent. This means that:

  1. Old classes and methods could be marked as deprecated, but are never removed.
  2. Things that would be changed in major versions are done in new namespaces or with new class names.
  3. To plan for this, perhaps the original namespace would use a versioning scheme.
  4. Any client that compiled and worked with an old version would compile and work with a new version.

To be clear, if clients write to implementations and not specifications, they might break with future versions, so documentation needs to clearly explain what is required and promised. The actual behavior should not be confused with the contract.

A common example of this is relying on buggy behavior. If I call a method and it has some bug, I might write code to work-around that bug. If a future version fixes the bug and my software breaks because of my work-around, that’s still considered a substitutable change as the bug was not a promised behavior.

In a talk on clojure specs, Rich Hickey called this concept accretion and railed against breaking change. I know that semantic versioning is too entrenched right now to change, but something like what Rich is describing would do a lot to relieve dependency hell.

Unblocking Coder’s Block

I started a podcast about a week ago and the first episode was an exercise that helped me overcome writer’s block. In the episode I said that I don’t get “coder’s block”.

This is more or less true, but I want to elaborate. I don’t mean that I can instantly solve any bug or program anything I want at will. I mean that I can make progress and do something.

When I have a problem I need to solve and literally have no idea what to do next, here’s are a few things that help.

  1. Write down everything you know about the problem
    1. Assumptions
    2. Goals
    3. Constraints
  2. Share this document with colleagues or talk to them about it.
    1. Are the assumptions and goals correct?
    2. Can a constraint can be loosened?
  3. Do experiments to learn more and update your document
    1. Automated tests and scripts that help reproduce the issue
    2. User tests or analytics
    3. Diagnostic logging
  4. Think bigger: Is there a fundamental problem that, if solved, makes this problem go away
  5. Think smaller: Can you solve a subset of the problem

And, sometimes it’s right to put a hard problem on the back burner and come back to it later.

Trying to Tell Stories

My instinct in writing is to just say what I believe to be a true or interesting thing in a direct way. Maybe give an argument or two why and call it day.

So, most of what I write is kind of like this: “Hey everybody, I think blah blah blah and one time I blah blah blah’ed and yada yada yada, it was great. You should blah blah blah too.”

And even as I’m doing this and re-reading my writing, I am thinking: “this is kind of boring”, but I didn’t know how to make it better. So I’ve plugged along because up to this point, I’ve decided that writing imperfectly is better than not writing.

But, today, (March 5th, the day I am writing this), is my first day as an independent software developer, writer, or whatever I end up doing. I don’t have the safety net of gainful employment to give my work meaning. It somehow has to come from me and my own projects.

And this blog is one of those projects, so it’s not enough to just write any more. Eventually, this blog has to be good, which I define as valuable to readers (as opposed to just valuable to me as a place to practice writing).

Over the years, I have seen/read/heard a lot of advice about writing, but one really stands out to me right now, and I am trying to really understand it.

It was a podcast episode of Scriptnotes (transcript), where Craig Mazin, the writer of the Chernobyl mini-series, explained what a story is. I recommend reading the whole thing, but the essential generator of story was an argument. This interests me because most of what I am writing is my side of an argument.

According to Mazin, to generate a story, you start with an argument with a true side and a false side. Then, you create a character that believes the false side and lives a stable, but imperfect life with this belief.

The story will make that stable life impossible and eventually change the character’s worldview such that they act in accordance with the true side of the argument. The details are fascinating, and I recommend you read the transcript (the back episodes are available on a paid subscription if you want to listen to it).

One way to sum it up is:

What is more interesting: “you know, if you lie to people, they might not believe you when you are telling the truth” or The Boy who Cried Wolf?

The first example is just advice (which is actually good advice, but it’s boring) and the second is a story. It has humor, it has twists, and it has an argument exemplified by a character living the opposite of the advice. It’s not exactly the same structure as Mazin’s, but it works.

Craig Mazin writes fiction, and a lot of what he’s describing is story invention. But, it applies to non-fiction as well (see his José Fernandez example). And, of course, if I think back to interesting non-fiction books I have read, they are full of stories.

But, I also know that long-winded stories (when you want actionable advice) are somewhat off-putting to me. I personally need to find the right balance.

In my own journey of “Stating advice directly, but in a somewhat boring way” to “Telling an interesting story that incidentally makes my argument”, I am really just getting started, but I will try to tell more stories of characters living the false and true sides of arguments I am making rather than just plain descriptions of the argument.

Accessibility First in Podcasts

I released a podcast a few days ago. I am doing this podcast partly to improve my writing and my ability to do Professional Performances, so it is scripted.

A side benefit is that it doesn’t take much to produce a transcript. In fact, I have a pretty good one before the podcast is even recorded.

Aside from accessibility, transcripts have many other benefits. SEO is a big one, but also, it makes it a lot easier for listeners to refer back to. And if they feel inclined to quote you on social media, it makes it a lot easier.

In any case, I’m glad that my process produces the accessible artifact first.

I Didn’t Have a Disk Drive

When I was 13, my mom got me an electric typewriter for Christmas. Luckily, it was broken, so we went back to Radio Shack to return it. When we got there, they were trying to move TRS-80 Color Computers for about $50 (the same price as the typewriter).

So my mom got me a 4k, chiclet keyboard version that hooked up to a TV. There was no storage included, but supposedly you could hook them up to ordinary tape recorders (I never figured this out).

The next Christmas, I got a Commodore 64, again with no disk drive. I finally got a disk drive for my birthday a few months later.

For a while, my home computer could literally do nothing except be programmed.

All of my personal programs lived on paper. I typed them in, played with them a bit, and then re-transcribed them back to paper. My most ambitious program in this time period was a very light Defender clone made from ASCII art.

It was like learning a musical instrument.

If I were learning piano, I would play songs over and over until I got them right. If I could compose music, I’d do it on paper and retranscribe often.

And doing this did at least accomplish the goal my mom had with her original gift. I learned how to type pretty fast.

Tech Career Tip: Drive Revenue, Not Cost

I have spent most of my career in software product development, meaning the code I wrote went into the products that made money for the company I worked for. I did a short stint writing internal software for the organization itself and the difference was very stark to me.

When you write software for your organization, it’s often to automate something and the driver is cost-reduction. The problem is that cost can only be reduced so much and eventually you are one of the biggest costs (in a budget owned by someone who likes to cut).

Revenue, on the other hand, can be very outsized compared to your cost. The budgets for product development are often expressed as a percent of revenue, so success increases budgets.

And, often, product development jobs offer some way to participate in that upside (options, RSUs, etc). Getting equity from an IT developer isn’t as easy.

It’s more complex than just “for-sale” or “internal”. When I was in FinTech, I wrote products, but my peers at banks, writing internal software, certainly drove revenue for their employers and were paid very well through generous bonus systems.

Pay is not the most important benefit. When you are critical to how a company makes money, you have bigger growth opportunities, more respect, and generally more clout. Product developers at software product companies have career prospects that lead all the way to CEO. That is not often the case for developers automating internal operations.