
What Does it Mean to be a Good Software Engineer?
Hello, early-career Software Engineers!
In this post, I want to precisely articulate what we should care about and why it matters when it comes to Software Engineering. I am amplifying the arguments that have been the most influential in my own early career.
I want to show a direct link between maximising value generation and the gestures we commonly associate with “being a good Software Engineer” - a pragmatic framework for Software Engineering. This framework attributes merit to the gestures of the Software Engineer based on the successes of their practical applications in generating value effectively.
I find that the majority of learning resources early on focus on tools and technologies to build software, but very few learning resources take a step back and take a look at the important lesson of what it even means to build good software.
What is my Job?
The what of every profession is the same - that is to generate value.
I then trade whatever “value” I generate for money. The more value I generate (if I can leverage this efficiently), the more money I will make. If I generate more value for my employer than my costs (in salary, training, etc.) and they can continue to leverage this value to the market, then there will remain a sustained position for me there - it’s simple arithmetic.
There are many configurations of how people go about performing this job (generating and leveraging “value”), resulting in a plethora of professions. Software Engineer is one value-generating configuration comprised of a set of complementary skills and tasks.
There are many dynamics to explore within this model (including the effects that uncertainty and risk play), however, I only want to highlight that the “what” and the “how” of your job are two different things - don’t mistake them!
Considering your job in terms of value generation means you can raise your eyes above your keyboard and look around at how to achieve this beyond tapping away at your laptop. There are many good reasons to care about maximising the value you generate at work, chief among which is that we tend to enjoy doing things that we are good at. Often, the better we make ourselves at generating value, the more rewarding we will find our jobs.
How do I Perform my Job?
At the time of writing, I work as a Full-Stack (web and mobile) Architect Developer at a software consultancy. When interfacing with clients, the way that I generate value is to “maximise the money I generate or save for the client” (this is a nice measurable metric).
These are the main value-generating activities I spend my time on:
- (Product Development) Develop features which meet the business requirements:
- with speed whilst not compromising future speed for changing requirements.
- (Methodology) Reduce waste in product development:
- identify the highest value features by measuring their impact on our heuristic for value (e.g. conversion rate when onboarding customers) and focusing on those;
- reduce development costs by focusing on the minimum viable product (MVP) first;
- problem-solving to increase team efficiency;
- reduce the batch size to deliver value as soon as possible.
- (Expertise) Consult with non-technical clients to avoid costly mistakes in technical decisions.
Notice, that there are several activities that I spend some time on because - although they don’t directly involve programming - they are generating value. A well-intentioned challenge about the value a certain feature brings to users can often save our clients time and money from focusing on it more than necessary. I try to spend my time working on activities that generate value and on which I am best placed to contribute.
Aside: value generation in your workplace
Larger companies demand their employees to become more specialised - “let the Software Engineers focus on programming and we’ll get a Product Manager to specialise in other value-generating activities which don’t require a degree in Computer Science”, goes the thinking! For this reason, Software Engineers tend to spend the majority of their time doing one highly specialised value-generating activity: programming!
I wanted to highlight that programming is just one sub-set of the value-generating activities that might make up your real job. From here onwards, I want to focus on answering one question: what does it mean to maximise the value we generate whilst programming?
(Or equivalently, how can we be good Software Engineers?)
Which Properties of a Software System Deliver Value?
There’s a simple formula for calculating the worth of a software system:
- Behaviour of the system (behavioural constraints): what it allows users to accomplish.
If it doesn’t solve a problem for the user → no users → no one pays for it → no value.
- Implementation of the system: how it was coded to work.
If it’s difficult to correct undesirable behaviour → software soon becomes useless.
α and β are coefficients describing the ratio of importance of behaviour to implementation.Behaviour is Independent of the Implementation
Consider two implementations of Hello World in JavaScript:
console.log('Hello world!')
// Full code: https://github.com/lowbyteproductions/JavaScript-Is-Weird/blob/master/output.js // Output: 'Hello world!' (()=>{})[({}+[])[+!![] + +!![] + +!![] + +!![] + /* [truncated] */ + +!![] + +!![] + +!![] + +!![] + +!![]))()
Both implementations have identical behaviour - the user can’t tell the difference between which program is producing the result (there are no noticeable performance differences).
If requirements change, and users will now only pay for a program which prints
Hello James!, the second program is rendered worthless by its implementation.By definition, if a user receives different feedback from two different programs, those programs have different behaviour. Varying implementations can change the behaviour of the system (e.g. two algorithms with similar but distinguishable results), however, there exist many different implementations which produce identical behaviour; therefore we can say, that behaviour and implementation are independent from one another.
I like using the words “behavioural constraints”, because it’s a useful way to frame that meeting user requirements is necessary but not sufficient for a good solution. There are almost an infinite number of ways to deliver the same features (different languages, frameworks, architectures); the only thing that non-technical stakeholders care about is the behaviour - we engineers are free to implement the system however we want - only being constrained by ensuring the system behaves as users expect.
Non-functional requirements are part of the behaviour of the system - these are still behavioural constraints of the system which we negotiate with stakeholders of the product.
Non-functional requirements change from system to system because behaviour is entirely driven by the value it delivers to users (if we care about performance, for example, we only care because the users are demanding better performance - we don’t care implicitly!).
By considering non-functional requirements as behaviour (alongside user stories), we also highlight them for negotiation with our stakeholders which prevents assumptions about them on both sides.
Defining Tech Quality
I’ve taken the time to define Software Craftsperson as someone who is working towards this goal.
Software Craftsperson: implements the appropriate tech quality standards to minimise the resources required to build and maintain the desired behaviour of a software system.
“maintain” is highlighted here, because the system is in maintenance far longer than it is in the “build” phase of its life, so there is potentially more value gained by focusing on the long-term.
It’s time to introduce my definition of Tech Quality (roughly from Clean Architecture). This puts “maximising value” into slightly more concrete terms:
Tech Quality: a measure of how tolerant a system is to changing the requirements without introducing defects.
How can we Maximise Tech Quality in our Systems?
Assuming we have two systems, A and B, the best implementation is “that which is the most tolerant to changing requirements”. This factor is more important than the considerations given to non-functional characteristics.
Consider A is more tolerant to changing requirements and B is more performant and more secure.
A is the better implementation as it meets the users’ expectations in terms of performance and security, and although B offers more performance and more security, the extra value delivered comes at diminishing returns (do the users care about an extra 0.5ms render time?). In delivering this extra non-functional value, system B is sacrificing the greater value - tolerance to changing requirements.
If both implementations are equally tolerant to change, then the one which can be implemented quickest is the best - if both are equally quick to implement, then the one which satisfies more non-functional requirements becomes the best implementation (delivers most value).
So, why is tolerance to changing requirements so important? Let’s consider a thought experiment.
Between two implementations satisfying the same behavioural constraints:
- The program that behaves perfectly but is impossible to change won’t work when requirements change, and I won’t be able to make it work. The program will cease generating value and becomes worthless.
- The program that does not behave correctly but is easy to change can be made to work quickly, and I can keep it working as requirements change. The program will perpetually generate the maximum value (once I make it behave as expected initially).
When we look at two implementations of a solution, we often have a gut feeling that one is preferable; now we have a definition and criteria by which we can make technical decisions on how to implement a system! This definition is directly linked to maximising the value you generate whilst programming!
When evaluating a solution, ask yourself, “Will this implementation enable another engineer to come back and easily change the behaviour of the system in the future?”. This is a metric we can use to orient ourselves when creating technical standards or reviewing code.
The Total Cost of Owning a Mess - Poor Implementation
If you have been a Software Engineer for a while now, you will have been slowed down by someone else’s code.
Once rapid teams can be brought to a snail’s pace in a short amount of time. As the team slows, they try to counteract this by cutting corners and so the mess grows. It’s easy to make clean code messy; it’s even easier to make messy code messier!
Eventually, the code gets knotted up - one change here breaks something unrelated over there. No change is trivial and the knots must be understood before new behaviour can be added. Those who understand the twists of this particular mess become bus factors (I’ve seen real-life examples where an entire system has been rendered practically unchangeable because the one guy maintaining it got “hit by a bus”).
As productivity grinds to a halt, management does the one thing it can think to do - “we need more developers” (hint: they don’t need more developers). As more developers join, they pile on more mess, making the team even slower.
Before too long we’re working with a legacy codebase. Developers resent being put on “that project” and eventually a coup d’êtat brews. Changes to the system become too expensive and a new team is formed to rewrite the system.
Initially, work is fast and developers fight to get on the project, however, it won’t be put into production until “it does everything that the old one does”. In the meantime, development continues on the old system - and so a race begins which may take years to finish; by the time that it’s over the “new” system may already a be legacy codebase itself!
So, how do good implementations (or, building software which is tolerant to changing behavioural constraints) deliver value? Good implementation means that:
- we can quickly and continuously improve the system to optimise the value delivered;
- the system is long-lived, so delivers value to users for longer;
- costs of maintaining the system are minimal so the net value is greater;
- developers enjoy working with the system so talent is retained within the company.
Tradable Quality Hypothesis: Competing Incentives
Hopefully, I’ve articulated the significance of implementing good solutions, but now that you care about Tech Quality, you start to come to blows with your stakeholders, “You’re not delivering what we want in time! Why are you spending all this time on ‘refactoring’? We don’t have time to be spending on automated tests when we have to develop the features that our users want”.
Sometimes, non-technical stakeholders expect that spending less time on “quality exercises” (e.g. refactoring/ writing tests) and more time coding features leads to increased speed. This is called the Tradable Quality Hypothesis.
In reality, the good Software Engineer is always doing everything he can to develop as fast as possible whilst not sacrificing future speed. These quality exercises are the things that enable us to continue coding new features quickly (you will know this if you have worked on a mess before). This impression of clashing incentives is an illusion - it’s just difficult for stakeholders to see all the things that aren’t going wrong in the future because we’re not rushing (it’s a thankless task!).
Professionalism - Who Champions the Implementation?
Although implementation has a significant impact on project delivery, I do not expect non-technical stakeholders to be champions of good implementation. This is because they don’t look at the code, and even if they did, it would be difficult for them to identify the quality of the implementation and precisely how it makes their developers slower. They want the system to behave a certain way and don’t really care how we do it (as long as we do it quickly). They often have no concept of how the costs of making a change can vary massively between different implementations.
It is our job as Software Engineers to communicate what is difficult for non-technical stakeholders to see and understand. Once clearly communicated, the decision which generates the most value can be made. Put it in terms of the things non-technical stakeholders care about - the cost of extra speed now is the hit taken to the speed of future delivery of all features for the lifetime of the system.
It would be a strange world if other professionals behaved like some Software Engineers. For example, when I get a plumber in to fix my sink, I don’t negotiate with her how many leaks I want in it by the time she’s finished - I accept that the professional knows how best to do her own job. In the same way, I doubt many stakeholders would be happy if I designed a system that was so complex that in a year’s time, it would be cheaper to start over completely than add new functionality to the existing system. Please don’t ask your manager for permission to refactor - imagine if your plumber asked for permission to fit a new valve in order to prevent an impending leak (if you’re anything like me, and know nothing about plumbing, you’d be confused as to why she was asking you how to do her job well).
Don’t let non-technical stakeholders dictate how you should be doing your job (even worse, don’t ask them how you should be doing your job - things will get really rough!). You shouldn’t need to ask permission to do a refactor - you’re the expert, you know best!
If we craftspeople won’t defend the implementation, who will?
Aside: can you still be a good Software Engineer whilst not maximising the Tech Quality in your systems?
Tech Quality Heuristics
When I ask someone why they made a technical decision, or what they value in Tech Quality, I get varying responses. Below are some things that people mention; see how all of these fall under the umbrella of increasing tolerance to changing requirements:
- write code that is readable and easy to understand (Clean Code);
- KISS (keep it simple stupid) - reduce the complexity of solutions;
- DRY (don’t repeat yourself) - reduce dev time to build the same behaviour;
- DRY (do repeat yourself) - don’t write an abstraction that increases coupling of the system;
- use the Law of Demeter - keep implementation details hidden;
- write short functions that only do one thing (Single Responsibility Principle);
- functions should SLAP(!) (Single Level of Abstraction Principle);
- create clean boundaries in the code - allowing old code to be easily swapped out;
- ensure high-level business logic doesn’t depend on low-level implementation details;
- write automated tests - reduce the likelihood of regressions (TDD’ers will find it ironic that I wrote about tests last!).
The way that we build quality into our systems is by applying the many patterns and practices (e.g. the Dependency Inversion Principle) that make the systems more flexible to change. Applying patterns in the wrong place or using an extensive enterprise framework where it’s not needed is anti-quality as it takes more time to implement and is more difficult to change. Most software engineers go through a phase of learning some cool patterns or frameworks and applying them everywhere - I’m not an advocate of this.
Putting it All Together: How to be a Good Software Engineer
The Goal: to become the best Software Engineers we can be.
What this means: maximising the value we generate whilst programming.
How can we do this: