15 things I wish I’d learned earlier about software engineering
On process – How the framing of software creation affects the outcome. How to think about the software development process.
On design – The importance of respecting the design phase of a software project.
On teamwork & communication – The dos and don’t of working on software with a team.
On implementation – How to approach implementing successful software. This includes ownership, prototyping, and abstraction.
On fixing – Nothing is ever perfect, and this includes software. Includes how to approach refactoring and technical debt.
Software development is a subtractive process
Software engineering (indeed, building products in general) on a macro level is not an additive process. It’s subtractive. You start with nothing, and then you build something. That something is well-defined before you begin building.
These diagrams I’m drawing fundamentally express project development as a subtractive process: you start with a mountain of stuff to get done, and by the time you’re done, someone will have done all of it. These diagrams visualize the abstract notion that there’s “all the work” and each person subtracts some of it from the space, and you’re done when everything has been subtracted.
But not all work is subtractive. Consider a factory worker assembling cars. If he assembles 2 cars in a day, he makes the company a profit; if he works harder and assembles 4 cars in a day, he makes the company twice as much. This is an additive sort of work. You can do the same work, but just more of it, and you generate more value.
Creative endeavors are not additive. They are iterative, convergent processes, and so they are subtractive: you work until you have converged, and then you stop.
The parallel to software engineering is clear: Writing twice as much code to accomplish the same end result doesn’t make you a “2x” software engineer. It makes you worse. Your job is to build a reliable, understandable, and stable system. To reduce a problem to its simplest essence and build that. Nothing more!
If you’re stuck, stop coding
If you feel yourself flailing, or getting flustered, stop coding. Hands off the keyboard. Go outside. Go talk to a coworker. Go read a book. Go home for the day. There’s no shame in this, and there’s no need to try to push through it (which you won’t be able to do anyway). Frustration does not breed elegance.
The worst thing you can do when you’re stuck is to keep coding. It creates the illusion of progress where none exists. It means you don’t understand the whole picture, and no amount of coding will help solve that. You’ll simply be creating a misguided liability.
Strive to build simple software (Or: Learn to recognize complexity early)
Software exists to fulfill the needs of its customers. Nothing more. As an engineer, it is your job to build software that accomplishes this in a simple and resilient way. Avoid cleverness. Avoid complexity.
Do not interpret this to mean “no abstraction”. That is not the case. When applied correctly, abstraction exists as a tool for simplification. Similarly, do not assume “simple” means boring. Also not true. Some of the most exciting paradigms are strikingly simple.
Software design takes time
Designing a software system is a process which simply cannot be accomplished in one pass. To claim otherwise is lying. Design is an iterative process. Your brain will naturally unfold and reveal more components of a system as you iterate, day by day, week by week. The design will gradually come into focus.
The passing of time is a magical thing when designing a system. Problems which seemed insurmountable and unsolvable will simply no longer exist as you iterate. Ideas which previously seemed genius will be revealed as abhorrent.
Put simply: Design is not something you can wish away faster by “thinking harder”.
You can’t wish away Design Process. It has been in existence since the dawn of civilization. And the latest clever development tools, no matter how clever, cannot replace the best practices and real-life collaboration that built cathedrals, railroads, and feature-length films.
Nor can any amount of programming ever result in a tool that reduces the time of software development to the speed at which a team of code monkeys can type.
Software design saves time and complexity
Fostering a healthy design process ensures your software solves the right problem correctly and elegantly. Encouraging a proper design cycle is one of the most important things an engineering organization can do.
Put simply: It is irresponsible to start coding until you’ve fully materialized and enumerated the needs of the project. Until you’ve written them down. You’ll discover after writing down your ideas that you’re not quite solving the problem you thought you were. You’ll find that the needs are different from what you held as true in your mind’s eye.
Writing is nature’s way of letting you know how sloppy your thinking is.
Design isn’t something you only do at the beginning
The design phase of a software project is not a rectangle-shaped step with a beginning and end. You don’t one day declare “We’re done designing. Time to code!” The design process generally begins without code, but the two processes should quickly begin feeding on each other: Your code is influenced by your initial design, and then your design is then influenced by the realities revealed by your code. It’s cyclic.
On teamwork & communication
Software development is a social process (Or: You’re the only evangelist for your ideas)
You can have the greatest design in the world, but if your team does not understand it, it will be resented. People inherently resent concepts they do not understand.
Do not be the “coder in the corner”, disappearing for days, coming back only once you’ve built a completed thing. You won’t want to change it. You’ll be attached to it. Others will be unsettled by its sudden appearance. This isn’t good for you, your team, or your customers.
Socialize your ideas early and often. Ensure other engineers feel like they have ownership over and input into the idea early. Familiarity and understanding breeds goodwill. Indeed, socialize your ideas before you’ve written a single line of code or a single word in a design outline. More often than not, a simple conversation with a colleague will result in a refreshing new perspective. For me, this is the first step in building anything, large or small. Be open to changing your opinion when presented with new and better ideas during this process.
Clear communication is key
The majority of your job as an engineer is fostering and creating clear communication. Communication takes many forms: Spoken word, written word, diagrams, documentation, code itself.
An engineer who can only speak in terms of code and API is an ineffective engineer. It limits their growth both personally and within an organization. If they can’t communicate ideas and opinions clearly, they will not work effectively with teammates. They’ll likely become flustered or frustrated when asked to explain the non-code benefits of a proposal. They will not be able to evangelize their ideas.
Communication skills are important when writing code, too: Code that doesn’t communicate its intention simply and understandably will quickly become a liability. Documentation that rambles or skirts around core concepts will be confusing.
Software engineers should write because it promotes many of the same skills required in programming. A core skill in both disciplines is an ability to think clearly. The best software engineers are great writers because their prose is as logical and elegant as their code.
Be open minded, even if you disagree
Being the most effective communicator in the world won’t matter if you’re communicating with people who look only for opportunities to shoot down ideas because they don’t agree with a certain component of them.
Realize that at the center of mis-guided idea, there is a well-intentioned core. Strive to extract and grow this core, not to shoot down the entire idea because of flawed things in the periphery. No one has to lose for an idea to win.
Karate is intimidating, don’t show it off
What do I mean by “Karate”?
- Showing off in-depth knowledge of obscure system functionality without need, reason, or explaination.
- Injecting higher level conversations with inconsequential points (Eg, implementation details) about the system at hand.
- Interrupting to correct things that are 90% correct, just to prove you know more.
Karate is a form of boasting or gloating. Engineers who employ karate generally build complicated things, so they have a chance to showcase this knowledge.
Interacting with someone who likes to show off their karate is intimidating. It makes the person on the other end of the conversation feel dumb and inadequate. They’ll avoid contributing to a project when this karate-slinger is involved.
That’s not to say having depth of knowledge is a bad thing. Instead, just use this knowledge to help and improve others when asked. Not to elevate yourself above others.
No matter how much “karate” you know, someone else will always know more. Such an individual can teach you some new moves if you ask. Seek and accept input from others, especially when you think it’s not needed.
Successful software has an owner
For any piece of software to be successful, it must have an owner. This person provides the overall direction for the software. They provide the vision. They provide the momentum. They’re passionate about it’s value. They tame the inherent nature of a product to grow organically, in all directions. A piece of software without an owner at the helm is listless and unguided, likely being bent to fulfill temporary needs.
Consider the owner to be an editor: They don’t write all the code, but they can criticize it, direct it, and reject it. They reduce it to its essence.
Prototyping is a great way to quickly test and clarify ideas without introducing risk
One of the best ways to accelerate the development of an idea into a well formed thought is to prototype it. Don’t get attached to this prototype, though: The prototype code should never see the light of day. Why? You should feel empowered to experiment and take risks in a prototype, risks that you wouldn’t take in code that’s shipping to production.
There’s two sides to encouraging prototyping, though: An organization should never apply pressure to engineering to “take a shortcut” and ship prototype code. It creates perverse incentives, and will likely reduce the quality of the shipped product.
Abstractions are Tetris
You know how in Tetris, when a line fills with blocks, they all disappear? That’s an abstraction. An abstraction means you don’t need to think about what’s below. Similarly, if you abstract improperly, the blocks never disappear, they just pile up.
Abstraction ties very closely to the design process. You can’t abstract correctly if you don’t understand what you’re trying to accomplish. If you can’t afford the time to clarify an idea, you’re best not abstracting it.
Think of technical debt like actual debt
You will always carry some amount of tech debt in a shipped product. This means you have a healthy balance between engineering rigor and product goals. You will never have enough time to do everything perfectly.
What’s important is to ensure tech debt doesn’t accrue in core or critical components. This will cripple you. You can visualize your tech debt like monetary debt:
- Some debt has a high interest rate, and you want to pay it off as fast as possible (if you ever do accrue it). Left to fester on its own, this form of debt will rapidly grow to many times its original size.
- Some debt has a low interest rate: You can pay it off slowly.
- Some debt actually makes sense to leave in place, if it enables you to spend more time on a more valuable component. An analogy here is a loan whose interest rate is lower than the rate of inflation.
Don’t refactor without a clear product-driven goal
If you’re going to propose a refactor of a component, ensure that refactor enables or accelerates a very clear product-driven goal, visible to customers. Never refactor for refactoring sake. “To make it cleaner”or “to make it more sweet” (yes, I’ve heard this one) is not a valid reason to refactor. You need a clearly defined and measurable reason why a refactor will improve the product.
What are some valid reasons to refactor? Reasons which are directly measurable and have obvious benefit:
- To reduce the number of customer-facing bugs by 50%.
- To increase stability by 30%
- To increase development speed by 40%.
- To increase performance by 30% on older devices.
- To decrease network usage by 25%.
A common theme across all these examples: All goals must be measurable. Ensure you track the success of your refactor against your goals. If you miss your goals, evaluate why.
This is by no means a complete list of everything I could have talked about. These topics could (and indeed do) fill books. However, it’s a good start. I also encourage you to read the quoted articles in their entirety.
TL;DR: Be clear and deliberate. Think before writing, and write before coding. Communicate clearly and pleasantly with your team.
Thanks for reading.