In interviews, I'm sometimes asked what I think the biggest existential threat to Ramp is, and I often say "doing mid-size responsibly." We've raised a lot of money, hired a lot of people since the early days of Eric and Karim's apartments, and are now, solidly, mid-sized. What does it mean to hit mid-size responsibly? And how can we leverage experience from a decade in high-growth tech companies as we sustain flight here?
When describing "the game" of being mid-size day-to-day, I often reference the Serenity Prayer:
God, grant me the serenity
to accept the things I cannot change,
the courage to change the things I can,
and the wisdom to know the difference.
A mid-size company cannot do the same things they did when they were small. They will need to adopt what feels like "big company nonsense" in order to grow. But this needs to be piecemeal, you don't hit PMF and suddenly operate like a FAANG. Every day, people at the company will have to have the courage to adopt a new Process or Attitude when they need to, accept it when they shouldn't, and have the wisdom to know the difference.
What's an example of a "responsibly mid-size" attitude? I'm going to talk about (and challenge) the idea of "clean code" in any codebase that has real responsibilities. This is adapted from a presentation I gave internally; it's not in response to the Bad Thing Being Described (in my 7 companies, I've never felt more confident in my peers), it was more given as a preventative, and a bit of "what I wish I'd heard when I was in my late 20's working at other high-growth startups." I hope it's useful to someone.
These observations are summarized from a Sandi Metz talk, Polly Want A Message, which in turn references other writing. I highly encourage anyone to watch the full talk, read her blog, or check out the book on OOP she co-wrote. First I'll mention that "bad code" is effectively inevitable.
She links to Michael Feather's Getting Empirical About Refactoring, which introduces the idea of "churn" as how often a file changes. Considering your most-touched files are interesting, but if you plot them with semantic complexity within the file, you get an interesting picture (and she adds the green trend line):
Most files in a codebase live in at most one extreme: left and low (few changes, low complexity), left and high (complex but rarely touched, think that one gnarly feature that doesn't require much development after launch), or right and low (touched a lot, but not complex at all: think config files).
Except that one little buddy on the top right: Changes a lot and complex?! I hope it's not important.
A consultant of many, many software shops, Sandi says in the talk:
If I looked at the churn versus complexity chart for your own app, I can tell you what's up in that right-hand corner. It's a class that's big, much bigger than the average size of the classes in your system. It's complicated. It has a bunch of conditionals in it. And it's about something that's super important to your domain. [...] If you're doing contracts, it's
My first gig was Flash Player; the most churned file was also the biggest,
splayer.cpp (the core player logic), which was around 22k lines when I was
there. Most of it was one big
She links a few graphics from Code Climate, a tool that measure code health on open source projects, and you can see almost every project has one of these:
You can find your 30 most churned files by running
git log --name-only --pretty=format: | sort | uniq -c | sort -nr | head -n 30
Really, watch the talk, or read her blog post presenting this material (she includes a few more points on Object-Orientation vs. simple procedures). I summarize it here to tell you "nasty" code is emergent to the point of being inevitable, and right where you don't want it. If it's in all these successful, useful codebases, maybe seeing it yours doesn't mean something bad is happening.
Another example, namechecking a very famous axiom: should you rewrite your app from scratch? Almost every CTO would give an emphatic "NO!", and they'd be right, and they'd probably cite the original Joel on Software article about it. But when you have an ick reaction to code, think of what Joel said on that too:
[...] you can ask almost any programmer today about the code they are working on. "It’s a big hairy mess," they will tell you. "I’d like nothing better than to throw it out and start over."
Why is it a mess?
"Well," they say, "look at this function. It is two pages long! None of this stuff belongs in there! I don’t know what half of these API calls are for."
[...] Old code has been used. It has been tested. Lots of bugs have been found, and they’ve been fixed. There’s nothing wrong with it. [...]>
Back to that two page function. Yes, I know, it’s just a simple function to display a window, but it has grown little hairs and stuff on it and nobody knows why. Well, I’ll tell you why: those are bug fixes. One of them fixes that bug that Nancy had when she tried to install the thing on a computer that didn’t have Internet Explorer. Another one fixes that bug that occurs in low memory conditions. Another one fixes that bug that occurred when the file is on a floppy disk and the user yanks out the disk in the middle. That LoadLibrary call is ugly but it makes the code work on old versions of Windows 95.
This is to say: "nasty" code will probably do things you don't think you need until you lose them. You're not a bad person for wanting to hit your skull against a wall trying to understand old code, but if you let an ick reaction slow you down, you're playing yourself. It's a job! Do the work! Your customers are counting on it.
Becoming a software engineer because you love coding is a lot like becoming a butcher because you love animals.— saurya (@Saurya) April 24, 2019
Last namechecking comes from this wonderful thread by tef, who also wrote the hilarious (and short) Devil's Dictionary of Programming. He has the full thread here (and a related one here), I'm cherry-picking some relevant tweets:
simple and complex are measures of how your understanding of the problem maps to the program, at the best of times— tef (@tef_ebooks) November 16, 2019
a lot of the properties we seek in code—simple, approachable, malleable, debuggable—are emergent properties, often accidents— tef (@tef_ebooks) November 16, 2019
a lot of the time, code ends up looking that way because it has to—we call it an abstraction when we like it, and indirection when we don't— tef (@tef_ebooks) November 16, 2019
don't talk about complex. talk about what needs you have and if the code will meet them. talk about responsibilities. anything else— tef (@tef_ebooks) November 16, 2019
the problem with people who eschew complexity, is that they often create something worse in their quest to eliminate it— tef (@tef_ebooks) November 16, 2019
my entire career has been spent, wasted, explaining the subtle and unintuitive consequences of self described simple approaches— tef (@tef_ebooks) November 16, 2019
there isn't such a thing as simple code, there's code that has no responsibilities, or code that doesn't manage its responsibilities— tef (@tef_ebooks) November 16, 2019
you can write a EULA in basic english but that does not make it any more approachable—simple things combined are not simple— tef (@tef_ebooks) November 16, 2019
simple, easy, clean, good—these are words that describe how the person feels about the code, often how much pride they take, not much else— tef (@tef_ebooks) November 16, 2019
The EULA example is a perfect illustration: real, working legal documents are structured the way they are for a reason, as are codebases you interact with.
Mid-size companies are where you start turning features into systems. It's when new engineers get hired to augment a previous engineer's code, without the context of why it looks that way. It's where some people thrived when they could keep everything in their head at once but may hit friction when that's not possible. It's when every "single conversation" turns into a game of Telephone, where signal loss becomes an inevitability.
With all that happening, it's a ripe time for an engineer to look at something in front of them and think "ugh, this is garbage." I write this for folks in other mid-size companies: allowing this to fester and inform how you do your work day-to-day is not only immature engineering, but also a quick way to company ruin. From Sam Altman's Startup Playbook:
A quick word about competitors: competitors are a startup ghost story. First-time founders think they are what kill 99% of startups. But 99% of startups die from suicide, not murder. Worry instead about all of your internal problems. If you fail, it will very likely be because you failed to make a great product and/or failed to make a great company.
The hardest part of mid-size is keeping your team together and sailing the ship as one. When you hear someone saying "oh, we're getting shanked by the Widget team again!" or "Christ, why can't Marketing be clear about what they want?!" your company is already dead, the doctor just hasn't called it yet. Allowing yourself to be doomerish about a codebase is chasing a siren song: remember how that story ends.
I'm not saying "code quality is for chumps:" please think hard and carefully about what you design and deploy! Forming opinions on code quality is an important part of becoming a seasoned engineer, and some people have made a careers promising you to teach you what "good code" is (though the advice is often dubious). Invest in your craft and form opinions on what works and what doesn't.
But, whichever opinions you land on, don't let your gut reaction slow you down, or stray you from the fact that the game is about delivering value to customers. The code should work for your company, not the other way around; and I hope I've persuaded you that almost every lasting codebase does this by looking "ugly," often where it really matters. Develop taste, but not at the expense of delivering.
(if you liked this, you might also like my piece on the cultural baggage around the phrase "legacy code")