Here are five more Guiding Principles I use when making technical decisions as a software engineer. You can also check out Part 1.
Just as before, this list is really a list of principles I use when making difficult technical decisions or mantras I use to snap myself out of being stuck - it's really not about just how I try to write good code (SOLID, DRY, etc) although there is a little bit of that as well.
Perfect is the Enemy of Good
When it comes to designing code, I think it's better to get started as soon as possible and make changes and modifications via refactoring as needed. It's better to get something up and working quickly, rather than spending time debating in front of whiteboards about the correct way to do things. In my experience, engineers in particular have such an affinity for elegance that we can get wrapped around the axle trying to figure out the perfect, most elegant solution.
I'm not saying to write shitty code, obviously. It's still important to follow good design principles like SOLID, the Law of Demeter, KISS, defensive programming, CLEAN, separation of concerns and so on. It's just that you don't have to get every little thing perfect, it's better to get something that's imperfect but works built and then refactor to perfection later.
Remember Gall's Law:
A complex system that works is invariably found to have evolved from a simple system that worked.
It's important to realize when you or your team have gotten into a state of analysis paralysis, which is one of the reasons I like Pair Programming so much - it's handy to have a second person around to recognize when you're wrapped up analyzing instead of building. Nobody really asked you to build the world's greatest, most reusable, most well-designed system on the planet. The company doesn't need the perfect solution, it just needs one that's good enough.
There are lots of ways engineers can get gridlocked doing analysis, and it's important to recognize all of them.
Don Knuth calls Premature Optimization the root of all evil. It can happen both in code/design, as well as architecture.
If you find yourselves talking about caching layers, circuit breakers, or geo redundancy before building even the first version of the software, you might be getting ahead of yourself. Those things are all just as easy to add later as they are to add now, so there's no reason to get wrapped up on these concerns early.
Obviously I'm not advocating writing inefficient algorithms when an efficient one is just as easy to implement, but if the code is substantially cleaner with something less efficient, leave well enough alone and just get it working. Even dumbass bubble sort is usually good enough, and it has the advantage that you remember how it works right now without double checking anything on Wikipedia.
Otherwise known as the Law of Triviality, this is when a disproportionate amount of weight is given to trivial concerns when designing something. The term comes from the fact that teams will tend to focus on the minor issues that are easy to understand, such as what color to paint the staff bike shed.
The more time you devote to making a decision, the more you need to periodically ask yourself "does this really matter?" A lot of times, it doesn't matter to anyone else on the team, it doesn't matter to your users, and it certainly doesn't matter to the company. If it only matters to you, you're probably being, you know, kind of a dork.
An entire team can bike shed as well. Recognize when your team is bike shedding and stop the conversation, drive it toward the things that matter. If people keep gravitating toward the trivial, it means that there's a lack of comprehension of the difficult decisions that actually matter. You either need to stop and get everyone on the same page about the challenging stuff, or you have the wrong group of people making the decision.
Premature reusability. Engineers have a tendency to want to design components to be as generic and reusable as possible, there's an old joke from Nathaniel Borenstein I'm fond of:
No ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.
Basic professional ethics would instead require him to write a DestroyCity procedure, to which Baghdad could be given as a parameter.
A really great example of over-engineering is found in Bob Martin's Agile Software Development. In it, Bob Martin and Bob Ross sit down to do the Bowling Game Kata, a programming exercise where you simply write code to calculate the scores for a bowling game.
The two engineers started talking about what classes they were going to have. There would need to be a
Game, which of course would have 10
Frame instances, each of which would have between 1 and 3
Throw instances. This seemed natural, like how you might answer a "design a the object model for a bowling game" question in an interview.
But as they tried to write tests to drive out the behavior of
Throw they found that there were no behaviors to those classes. A
Throw is really just an
int. In the end, they wound up with a simple
Game class and nothing else, with a handful of methods on it to say how many pins were hit, and a method to get the score.
Don't start any large endeavor with a mind on generality and reuse. Follow the Rule of Three - make everything designed for single use and naturally you will eventually discover reusable components falling out when refactoring after you've done the same or similar things in multiple places.
Exception - Architecture
It's important to note that there is one exception to this idea: your code design can be just good enough. But your system architecture needs to basically be perfect from the start. This can be extremely difficult to get right, but it's important, so a little bit of analysis paralysis is somewhat forgivable.
When it comes to code design, evolutionary design is the way to go - just build it and evolve it. But for architecture, get the team into a room with a whiteboard and hash out the details before you start building. Evolutionary design, up-front architecture.
How do you know the difference between design and architecture? One analogy I'm fond of is that architecture is strategy while design is tactics. Doing the right thing vs doing things right. That's a helpful distinction but I find myself most fond of Martin Fowler's definition:
Architecture is the stuff that's hard to change later. And there should be as little of that stuff as possible.
Anything that would be extremely difficult to change later on is something deserving of a substantial amount of upfront analysis. The language you choose for your code is architecture because changing it would require a full rewrite. If you're using a highly opinionated framework like Rails, Grails, or something that spreads throughout your entire codebase like Spring, that's architecture.
If you go with microservices, lots of decisions that are typically architecture suddenly become design, because you could swap one microservice for another easily, or quickly swap out the language or framework of one service. However, now the contracts between services - which would be easy to refactor if they were all in a single codebase together as simple classes - cease being design and become architecture. And of course the decision to use microservices or not altogether is architecture.
The data store you use is likely architecture. It can sometimes be easy to swap out MySQL for Oracle if you're using a strong database abstraction layer or relying on JPA or ActiveRecord something similar, but as your data needs grow you'll quickly find yourself using customized queries or perhaps even stored procedures, and migrating becomes difficult. Even if you choose something like Postgres and try to keep the option open to switch to Oracle or MariaDB, you're still picking a relational database at all, and switching to a NoSQL store would be extremely difficult so no matter how you slice it, it's architecture.
Public-facing APIs are a strange middle ground. Once you've decided on the APIs, they're impossible to change without affecting your users, so they're architecture. However, you can introduce a new API version later fairly easily, so it's not that hard to change your mind, making it sort of design? Of course, the WAY you version the APIs in general is architecture, because if you provide no facility for versioning early on it becomes difficult to add a new version later.
Overall the dichotomy is subjective so you need to use your best judgement, but what's important is that you don't spin your wheels making something perfect that could be perfected later if it can be good enough now.
If You Break My Code, It's My Fault
I've blogged about this one before, under the more provocative title "I Broke Your Code, and It's Your Fault". In fact, there was even a lengthy reddit discussion about it in which folks tried to decide if I was clinically insane, or just a regular moron.
Hyperbolic title aside, I still stand by the original point. Even if someone else does something as annoying as change the interface I was depending on in my code, it shouldn't be possible for them to so thoroughly break the code I wrote without SOMETHING telling them that they did so. All it takes is one failed test to say "hold up, you broke shit."
When I push code up to the shared repository, it's my job to ensure it works, not QAs. But it's also my job to ensure that a junior engineer or a new hire can't just break it without something telling him or her it happened. When I write code, I try to imagine, what would happen if some other engineer came in and modified the class I just wrote, maybe didn't understand why I was doing
-1 somewhere, and so they just removed it? Would that be an annoying thing to do? Sure, and I would hope that the other engineer might ask me why I was doing it if I failed to make it obvious from the code itself. But maybe this is years from now and I'm not even at the company anymore, so they remove the
-1, or they think my code sucks so they rewrote the entire function from scratch. The instant they do that, a test I wrote somewhere should fail (hopefully with an explanation of why it needed to be the way it was).
By writing my code like this, and creating what reddit argued is too many tests, I am encouraging the other members of my team to embrace fearless refactoring. Don't like how I wrote something? Refactor it, and don't worry about breaking anything - I wrote enough tests to ensure that you can't. Is it possible I'll make a mistake and fail to cover something I should have? Of course it is, but when this happens, the refactor-er in question did me a favor by highlighting a mutation that I missed.
Top comment on that thread questions the wisdom of being happy that an app breakage highlights a missing test we can add. The commenter says to try telling the client about your unit test suite while they're losing customers left and right due to the bug. I guess that's a fair poi-- wait, what? You're developing applications where breakages and bugs can utterly destroy your company, and you're not writing a metric ton of tests? That's some serious Evel Knievel shit right there. Um, Evel Knievel was a stuntman in the 70's. Er, the 70's were a decade about 30 years before Spongebob first aired. Nevermind.
Look, the safety net of an overabundance of unit tests combined with some high-level smoke tests to ensure that basic functionality is always working should give the entire team the freedom to refactor and rewrite anything they don't like. If everyone on a team is able to adopt this attitude, the end result is code that is incredibly clean. If the team isn't fearlessly refactoring, and they're afraid to make tiny changes and improvements because something might break somewhere, your team is hamstrung. Modules start to have a "here be dragons" vibe, with everyone afraid to improve them and so they rot until your entire codebase is rotten and you think you need to rewrite it (we talked about that already though).
I'm not saying it should be impossible to break my code. Changing the interfaces of things I depend on, or literally going in and modifying what I wrote could easily make it behave incorrectly. I can't stop that. I'm saying it should be impossible to break it without a test automatically telling you that it happened.
When you actually imagine that another engineers might come in and accidentally (or maliciously) modify your code, your tests get much stronger. You'll find that your assertions are better when you try to guard against this sort of thing, which is really what unit testing is all about. Lots of people track coverage for tests, but coverage basically just counts lines hit during the testing phase. You could write a suite of unit tests that actually hits every single line of code, giving you 100% code coverage, but makes no assertions whatsoever. Your coverage is high, but your tests are borderline useless in this case. Raw coverage isn't what I'm talking about here.
It's not about how many tests you have or how many lines they cover, it's about how strong the tests you have are. And approaching your tests with the attitude that it should be impossible to break your code without a test failing is how you make them strong.
This is probably the strong opinion I hold that comes closest to zealotry for me. As a counterexample, I really love pair programming but I left the gig where I did it regularly and took a job where the team really didn't like pairing, and I adjusted fine to not pairing. I usually write my tests first and enjoy the TDD red-green-refactor cycle, but there are times when I suspend this practice and write tests later. There are plenty of things I really love doing that I'm more than happy to stop doing as the situation demands, but I don't think I can go back to not testing at all, and I might be unwilling to listen to arguments to convince me to.
At this point in my career, the level of physical discomfort I feel writing code with no tests at all is unbearable. Not too long ago I was extremely busy with one task but was forced to switch gears to implement a small change I didn't really agree with to an unrelated part of the code. As some kind of juvenile form of protest, I half-assed the code and wrote no tests, just to get it done and off my plate so I could go back to what I was doing. I pushed it up to the central git repo and felt so uncomfortable with what I had done that I lasted about 60 seconds before going back in and writing some tests to cover the change and explain why in the test case. My rebellion was brief, I am not a badass.
I've heard of places where bosses will declare that unit tests are a waste of time that slow down development, and I genuinely don't think I could work in a place like that anymore. Ten years ago and I wouldn't have cared, but today it just seems like an impossible request that I don't write tests, like asking me to drink lighter fluid or something. I've fallen into such a comfortable cycle of code-a-little, test-a-little that eschewing the process feels completely unnatural and foreign; whiteboard coding interviews seem so bizarre to me now, I'd never write so many lines of code without tests at work. My god, there's an
if statement in it, that's two tests!
My code design has vastly improved by thinking about testability. Once upon a time I'd have used
new FileReader("whatever.txt") without a second thought, but viewing code through the lens of testability made me realize that all of those things are subtle integration dependencies on the underlying system. Figuring out how to write unit tests for code that depends on random number generators, a clock, or the filesystem has forced me to consider things as candidates for dependency injection that I'd never have considered without those tests. Even if I were to delete those tests afterwards, the code is still cleaner and better for having been designed with them in mind.
If You Hate It, Do It More
This one is easy to say, but very hard in practice to commit to. Basically, whenever I find myself dragging my feet on something I don't want to do, I need to sit down and ask myself why I hate doing it. Chances are, when I get to the root cause of my disdain or anxiety, I find that it's because something is extremely inefficient or error-prone.
Hate performing deployments? Why? There's a good chance it's because it involves a bunch of manual steps, handbuilding artifacts and manually uploading them somewhere, then shelling into multiple boxes and executing commands. The desire to get away from anything unpleasant is very strong, but it's these situations that would benefit the most from doubling down and doing it more often.
If you're deploying every quarter because it's such a pain, you need to start deploying every month. If you still hate it, every week. If you still hate it, every day. At some point you'll hit a point where you say enough is enough, and if you're going to deploy this crap every day then it needs to be easier. And that's when you start developing deployment pipelines and writing automated scripts. The more you do something you hate, the better you'll get at doing it, if only to keep your sanity.
Hate provisioning machines? Start adding and removing boxes from clusters on a regular basis. At first it will be difficult and annoying - that's good, that's what will make you better. In no time you'll be using OpenStack or AWS, augmenting setup with Puppet or Chef, or maybe even containerizing your entire process with Docker. Your infrastructure will be better for it, everything that you hate doing is likely a weak spot in your development.
Hating something is your brain's way of telling you "this sucks," but instead of responding by hating it, respond by taming it. The more you do it, the easier it is to figure out which parts suck the most, and how you can improve them.
One of my favorite examples of this is Netflix's Chaos Monkey approach. Dealing with failure was such a negative experience for Netflix that they started doing it all the time, so often that they built software that would randomly fail-out nodes, clusters, or even entire regions. It forced Netflix to revisit how their software works, and handle failure better. What came out the other end was a vastly superior product. And also "Daredevil".
This principle is tough because it's a lot like cleaning an incredibly messy room or a trainwreck of a garage. Things start pretty bad, but the real issue is that things have to get worse before they can get better. Only by embracing the things you hate doing the most do you force your own hand, resulting in something that can do the horrible job you hate automatically, on-demand, and quickly.
Yes, this principle applies to basically every aspect of your job. This one is particularly tough for me but: if you hate meetings, have more of them. Start having daily meetings if you need to. In so doing, you and the rest of the team will discover exactly what it is you hate about meetings so much. The only way to really figure out EXACTLY what you hate about meetings is to expose yourself to them so often that it becomes immediately apparent what doesn't work about them for you.
Once you've identified what meeting dysfunctions make you despise them so much, it's easier to fix those things and make meetings more enjoyable. Honestly, I hate meetings too but I need to ask myself: geeze, why? Should it really be so unpleasant to meet and chat with other engineers I respect and enjoy working with? Are we really such misanthropic jerks that we can't enjoy exchanging ideas? And don't say that the reason you hate meetings is because they prevent you from doing Real Work, I've already talked about how dumb that is.
After you and your team realize what doesn't work about meetings, you can take steps to address them until meetings aren't something you despise. And once you don't hate it, the inverse of the rule applies: if you like it, you can survive doing it less. Dial your meeting schedule back down once the thing you hate is not meeting.
Be the Worst Person in the Band
I got this from Chad Fowler's "The Passionate Programmer" who in turn took it from jazz guitarist Pat Metheny, who said:
Always be the worst guy in every band you’re in.
This idea has resonated with me ever since. Is it uncomfortable to be the worst person on the team? Yeah, it sure is. And it's this discomfort that will drive you to be better. When you're the best person in the band, you walk around with tons of confidence but you aren't learning anything and you aren't improving, because nothing is driving you to. When you're the worst, you have to step it up.
One of the great things about this career is that it's absolutely impossible to ever know all of it. It's growing and new tools and ideas are being added at a rate faster than you can possibly learn them. I can see this being stressful for some people, but it's my favorite thing about it. There's always, always more stuff to learn and improve. It's like being a bookworm and walking into a library of infinite size.
Nothing makes me want to learn more and be better than being surrounded by people who are better than me. I've worked plenty of jobs where I was the worst guy in the band, and plenty where I was the best, and I always come out of the ones where I was the worst guy in the band feeling like I just spent the entire time leveling up like crazy. Being the best fills you with confidence, which is nice on an emotional level, but it's nowhere near as satisfying as coming out the other end of a job a vastly improved person.
I've modified this slightly to be the second worst guy in the band. Being the truly worst can make you feel useless, like you're not making any valuable contribution. Plus it actually helps to be able to mentor someone, one of the most effective ways to learn something is by teaching. In any case, definitely don't be the best person in the band.
Another way I've heard this phrased comes from Scott Bain as quoted in Beyond Legacy Code:
Always strive to be mentoring someone and to be mentored by someone.
You can subscribe to all the blogs, read all the books, and attend all the conferences, but nothing will help you learn and keep up with the ever-changing world of software development like working every day with someone better than you. The more people that you're working with that are better than you, the stronger this effect.
Your First Loyalty is to Your Users
This one might be a little controversial, and proudly proclaiming it on my blog might make me unemployable. But at the end of the day, I as a software engineer answer to an authority greater than my product owner, my boss, my VP, my CTO, or anyone else who signs my paychecks: I owe my users quality software. If I wouldn't be willing to attach my personal cell phone number to the feature I'm developing, I shouldn't write it.
I have, on more than one occasion, gotten into a heated debate with a product owner or even a supervisor about a feature I was asked to implement. Often, this stems from situations where the people who are USING the product aren't the ones PAYING for it, and the client's higher-ups are writing the checks for features that their underlings using the product might dislike. I've found myself usually able to win these arguments by helping the product owners understand how unhappy their users will be, and pointing out that happy users will eventually leave their current company and become a sales lead at their next gig, but the most heated and intense arguments I've been involved in at work always stemmed from me advocating on behalf of the voiceless users who would end up on the receiving end of antagonistic features.
Unlike most the other principles on this list, this one won't result in better quality codebases or more SOLID or testable designs - it actually affects the product I build at a business level. I will never do anything half-assed, never lie or mislead my users, never take advantage of them, and never intentionally create a negative experience for them because it will line my or someone else's wallet. This is especially true when my end-users are not engineers themselves - they have no power or control in this software-centric world, so taking advantage of the power imbalance is particularly unethical.
I'm not arguing that everything you build needs to make the world a better place at some cosmic level, or that you need to be carbon neutral in every facet of your life or anything like that. I understand that sometimes you need to pay the bills. But what I'm saying is to never, ever forget that at the end of the day some poor schmuck is going to be using the thing you're building, and this person has people who care about him or her as much as you care about your loved ones. Imagine your mother or husband or best friend using your software - do you feel good about yourself? If not, don't build it.
Don't write software that tricks emissions tests just because your asshole boss told you to. Don't write copy protection schemes that phone home with a user's private data just because your CEO thinks he's entitled. Don't develop code that opts users into monthly charges if they are dumb enough to trust you when using your product, there's no such thing as a "stupid tax", your stupidest users need your advocacy the most.
Just remember, someday there might be a scandal and a court case that involves engineers being held accountable for the features they built, and "I was just following orders" may not be enough to save you. Be proud of what you create. It's not enough to assume the guy who ends up maintaining your code will be a violent psychopath who knows where you live. - assume that your poor users are as well.
I think there's a tendency for developers to "just do what they're told." The users and their experience is the concern of the product owners, marketing types, salespeople, and other stakeholders - the developers just build the software according to the requirements, right?. In all honesty, I wish that this was a safe mindset to adopt - I'd rather concern myself only with the code and my fellow developers who have to work with it, and leave the features and user experiences up to other people. But time and time again, I've found that for whatever reason the folks in those positions lose sight of the user experience and request antagonistic features. At the end of the day, the engineer is where the rubber meets the road - we're the gatekeepers on what actually gets created, so we're the last line of defense before something goes out the door that will make the lives of users worse. Product Owners and marketers can draw boxes and do photoshop mockup designs all they want, but the engineers are the only ones with the power to actually build the stuff users will be interacting with, and as the sole wielders of this power, we have the responsibility to consider those users even when others don't.
I feel like there are more things I wind up saying a lot, but one of the most challenging parts of writing up this list was even stepping back enough to realize which things could be written down. When you live by certain ideals long enough, they become so ingrained that it's hard to even remember what the principles are. Most of the ones on this list I realized only because I've been called out by other people for saying them so often.
Anything missing? Any principles that you live by as an engineer? Leave a comment, I'm curious what other people see as their Software Engineering Golden Rules.