Reusability
When I started learning BASIC as a child on a Timex/Sinclair 1000, my code was a wonderful mess in the “big ball of mud” sense. I was having fun and it didn’t matter exactly how everything was put together. I hadn’t yet developed the wisdom to think about code at that level anyway.
When you write like that, nothing is reusable and every new program starts with a blank canvas. Of course you end up repeating yourself when you need to do something you did before. But that’s probably a good way to start because it drills in the basics and gives you opportunities to improve as you repeat yourself.
That only gets you so far. At some point you need to reuse your code or lean on someone else’s. That’s why libraries and frameworks exist and abound for every major language, for opinionated values of “abound” and “major”.
I’ll just lump them together under the word “library” today.
Overgeneralization
Once you start writing a library, it’s easy to catch the generalization bug. You assume, incorrectly, that your “customers” think the same way you do, and that they’ll appreciate your cleverness as you make your library solve every possible problem in its domain. Before long, your thought process is taken over by “what if the developer wants to…” And complexity creeps in.
Generalization is fine. Heck, that’s the whole point of reusable code. The line where it becomes overgeneralization is situational and fuzzy. When you cross that line, your library becomes less valuable.
Overgeneralized -> Complex -> Less Valuable
Trying to cover every possible situation with your library leads to complexity, which can leak into every interaction that developers have with your library.
“Doing the job” is only part of the value that your library provides. A good Developer Experience (DX) is the rest. Your library is a means for the developer to accomplish some other goal, whatever that is. Any friction imposed by a library impedes the developer from reaching that goal.
Extra friction can come from lots of places, like:
- a complex API, with:
- a large collection of data types and functions or methods
- specific calling sequence requirements
- extensive control flow needs around calls
- lots of “Manager” or “Service” ceremony
- leaky abstractions
- unclear or overly abstract/clever naming
- unnecessary dependencies
- on libraries
- on external services
- introducing new ways to do things when industry conventions exist
- poor documentation
- not enough documentation
- insufficient examples / no “quick start”
- too much documentation (yes, that’s a thing)
- documentation that is difficult to navigate or search
- documentation written from the library’s point of view rather than its consumer’s point of view
This friction adds up fast in the form of cognitive load on the developer. And the more load imposed by your library, the less capacity the developer has to solve their real problem.
Simplicity -> More Valuable
On the flip side, if your library is simple, then everything becomes easier:
For your users:
- Easier to use
- Easier to understand
- Easier to replace with another library later (this is valuable!)
- Easier to trust
For you:
- Easier to describe
- Easier to test
- Easier to maintain
- Easier to document
A Matter of Taste and Judgment
Sometimes your library really needs to handle every possible situation. If so then:
- This post’s advice may not be helpful
- Good luck!
Otherwise, it’s probably worth pulling back and asking yourself:
- What are the minimal needs from my library?
- What are the most common needs from my library?
- What is the most common sequence of events for my users?
- What are the most common problems that existing libraries introduce?
- What are the most common errors that can come up during use?
- What are the trickiest errors that can come up? The ones that are hard and important to handle correctly?
- What kind of “footguns” exist in this problem domain? Is there anything that users need to explicitly be told not to do? Can these things be made impossible?
- What vocabulary or metaphor do my users already use in this domain?
- What’s the simplest “happy path” (i.e. when no errors occur) through the functionality I’m trying to provide?
- Can users with more complex needs still benefit from what I’m building even if I don’t do all the work for them? How?
- Can it actually be improved by removing anything? Including requirements?
Your definition of “done” should depend on these answers. There’s judgment involved - e.g. where do you draw the line on “most common”? How do you even know what’s most common? The sweet spot for how much you should address each item is situational. 80% might be a good starting point for each. 90% for some items and 60% for others might be right for your project. Be thoughtful, explicit, and deliberate.
👴 Speaking from experience...
If you are writing your library for personal purposes or for fun, and enjoying the freedom of working without the “requirements” you deal with at work, I have bad news for you: you still have requirements, even if they're just in your head. They need to come out.
Anyone who reads your code, including future you, will wonder why you made certain decisions. Writing out your goals, requirements, assumptions, and non-goals, even as a short bullet list, will make everything much more understandable later.
Future you will thank present you.
Painful Lessons
If you tell a toddler not to touch a hot stove, they might listen. They might even remember the warning. But if they actually touch one, they’ll definitely remember.
That’s where this is coming from: I’ve touched the stove over and over again in my career. In my defense, I don’t remember getting a lot of warnings about these things, so I’m writing them down for others. They might listen. They might even remember. 🙂
I still need to be reminded of this from time to time. I’ve been programming for forty years, and just yesterday a colleague eliminated a whole pile of headaches for me on one specific requirement by simply asking “what if we just… don’t do that?” This wasn’t a lazy call, it was the right call, both for the tool and for the users. Palm, meet forehead. (and great call, Jim!)
The best library might not be the one that does the most. It might be the one that gets out of the way. Good luck with yours!
The same thinking can apply to larger software beyond libraries. As one example, I invite you to take a look at ChatKeeper, a desktop tool for turning ChatGPT exports into local Markdown files and images you can own.
