Software Engineer Mindset – Looking for a Job Well Done

Reflecting on my years as a Software Developer, I realized I had a few mindset shifts along the way. The mindsets drove me to find different tools, best practises and procedures. They defined how I worked. Those mindsets ultimately come from the same question: What is “a job well done”.

Vision(Job Well Done) -> Strategy(Mindset) -> Tactic(Principal/Tooling) -> Execution(Day to Day Work)

I broke my progression into 4 stages, in hope to give you some idea on where to go next. Since I have a sample size of 1, I can’t say if everyone goes through these stages necessarily. And it’s totally possible that you don’t agree on some of my believes listed here. In that case I hope at least they will give you a different view and inspire thoughts.

Also just to point out, this series of mindset focuses on “How do we make good software”. There’s another side which is “How do we work well at making software” which is your SDLC/agile practise. That’s out of scope of today’s discussion.

Stage 1: Make Things Work

What’s a job well doneDelivered fast. Functionality completed. No bug
Mindset – Work fast, deliver more, no bug
– “If it ain’t broke, don’t fix it”
SkillsBasic programming skills. Basic automated test skills

Status

The first stage is “make things work”. The goal here is to work fast, deliver more. At this stage, code quality equals to “no bugs”(or in reality, no service disrupting bugs). Occasionally senior developer will jump in and give refactor suggestions so that “code quality is better”. Those suggestions are usually followed blindly, without thinking too much on the reasoning behind them. Programming principals might be brought into view here and there, but they weren’t seriously considered since they don’t contribute in “working fast” on the surface.

This mindset has a natural extension: “If it ain’t broke, don’t fix it”. After all, if it’s working already, it fits the job well done definition: it’s functionally complete, and there’s no bug(or there are, but we have decided that we can live with them). There’s no work to be done here.

At this stage, the tools that enable delivering “a job well done” would be basic programming knowledge, basic automated test skill(to reduce bugs and pass code review threshold), and the ability to read documentation – including technical design(in a waterfall sense) and framework/library manuals. These skills remains relevant throughout a developer’s career.

Problem

Clearly, with this mindset, a developer will more likely to create ill-structured code which increases the cost of future development.

How to Step Up

If you think you’re in stage 1, I would suggest you pay attention to code reviews and pair programming sessions. Look for the places where senior dev would do differently than you, ask them why, then try to simulate their way of thinking. By this simulation you’ll build a mental model that works in similar ways, and soon you’ll be able to utilize that mental model to process the tasks given.

If you are mentoring some1 in stage 1, same concept: explain the why to get their consensus, then provide opportunity to let them practise the stage 2/3 mental model on real problems, until it becomes natural to them.

Stage 2: The Beauty in Code

What’s a job well doneImplement feature with “clean” code
Mindset – “Clean code” / “Beautiful code”
– Coding is an art
Skills – Programming principals – taken in the literal sense
– “Opinionized” technical decision making

Status

I call this stage “The beauty in code”. This is when a programmer has seen enough code to tell the bad ones from the good ones. By combining the industrial best practises with their own experience, they form their own aesthetics. It’s a step up from stage 1 because the best practises are genuinely appreciated and followed(albeit blindly). As a result, the code produced is of better quality comparing to stage 1.

I spend over 10 years in stage 2. During those years, I picked up best practises(with a lack of better word) from IoC, MVC and Design Patterns, to Programming Principals like SOLID, Hi-Lo and DRY, to Microservices and Event Driven Architecture. You might notice as the time goes, the best practises in the list become more abstract and affect a bigger scale. But ultimately they are all “How to”s that directly guide tech decisions. There’s no “why” in these principals(or rather, there are, but they are overlooked). This missing of “why” is the key difference between stage 2 and stage 3.

Problem

You might notice I use words like “beauty”, “aesthetics” and “art” to describe the mindset in this stage. It is because at stage 2, code quality is subjective. This is a natural outcome from dogmatizing the best practises. Imagine several stage 2 programmers working together. They are exposed to different principals in different time from different channels. They will develop bias, favoring some principals over others. This bias creates different “school”s of practises(e.g. DRY vs WET). Stage 2 programmers from different school would think each other’s work “OK-ish but could be improved”, which translates to “Better than stage 1 but should have followed my school”. This bias creates tension within team, creates blocker on knowledge sharing and reduces the options to solution design.

You might also notice I didn’t mention anything about automated test in this stage. This is because the appreciation to automated testing at this stage largely depends on the programmer’s exposure to SE methodology. I remember I thought unit tests are just formality in the early part of stage 2, and later I became obsessed with test coverage. Neither of these are healthy.

How to Step Up

If you feel you’re in stage 2, you can probably figure things out yourself by asking “why” behind the programming principals. However a potentially easier and more pleasant way is to follow the masters in the industry. These are my favourites:

  • Dave Farley’s youtube channel
  • Kent Beck’s blog
  • GOTO Conference’s youtube channel(not everything is worth watching though. I’d suggest skipping anything that’s tied to a specific technology)

They might sound quite abstract at first. But that’s what we’re missing at stage 2: a holistic view of the entire software development practise. I’d recommend keep watching/reading/listening even if it doesn’t make sense, and try to simulate their thought process, actively seek opportunities to use their mental model to explain and guide our technical decisions.

If you’re mentoring a stage 2 programmer, it’s important to have discussions around the “why” – What does those best practise give us, what’s the tradeoff when we choose one over another, and how to compare different options on the “why” level. By demonstrating this process they will hopefully start to use the “why”s to guide their decisions, rather than following best practises blindly.

Stage 3: Optimize for Change

What’s a job well doneOptimize cycle time for long term
(Cycle time = the time span from a feature request lands in team to the feature released to prod)
MindsetGood architecture means adaptable to change
Principals – Optimize codebase for Readability
– Automated Testing
– Domain Driven Design
– YAGNI
– Type 1 vs Type 2 decisions

Status

This stage is when a programmer become aware of the trade offs of the technical decisions they make, and start to think from one level above – Why do I pick this approach over the other one, what is the tradeoff, and ultimately, what is considered high quality code. Eventually they will come to the same answer: Good code means easier to change.

Easy to change is the ultimate goal of every software principal, strategy and best practise. It is also objective, meaning for each technical decision, there is always a definitive answer. The problem however, is that we don’t know what kind of future changes we are going to face, therefore we often cannot tell which one is the right answer at the time we must make a decision. Coincidentally, Pragmatic Dave Thomas recently had a talk in which he raised the same question. His answer is to train your subconcious, then rely on your subconcious to guide your decision. My answer is Domain Driven Design. Aside of these, we must be aware that “we don’t know yet” can be a perfectly reasonable answer at times.

Techniques

Here are my guidelines when making a technical decision:

  • Domain Driven Design: The guiding principal to finding the boundary between domain entities and services. My theory on why it’s effective is rooted in how the human mind functions – we think with defined concepts and by forming relationships among them. Concepts, once established, are almost atomic in our mind. This means even the innovative ideas are often operating along the edges of existing concepts. Rarely do they completely upheave the concepts themselves. By aligning our code to the concepts(maintaining a 1-to-1 mapping between concepts and domain entities), we significantly reduce the likelihood of future changes causing a misalignment between the business logic and the codebase.
  • YAGNI (You Ain’t Gonna Need It): Assumption to future changes often misses the mark. We should be mindful of, and actively resist the urge to optimize the code at this moment for hypothetical future needs. Premature Optimization falls into this category too.
  • Type 1 vs Type 2 decisions: from Jeff Bezos. If we cannot make decision between 2 options because “we don’t know yet”, opt for the one that’s easier to revert. If both are easy to revert, just flip a coin and pick one – it’s often cheaper to pick one and test in the market than stall in analysis paralysis.

And here are some generic tips that helps on keeping the code easy to change:

  • Optimize codebase for Readability: Too often we find the code we wrote 2 years ago unreadable. This is because the code itself only explains “how are we computing the result”, it does not explain “what are we computing” or “what do we want to achieve”. A piece of code would be significantly easier to understand if we explicitly explain the business rule in the code itself, often in the form of function names and variable names. If we push this standard to an extreme, good code should be readable to non-developers. Most of my code review were focused on variable names and function abstraction. It must feels like I’m nit-picking on my coworkers. But I believe the names carries more impact than any programming principal.
  • Automated Testing: I once told my team “test is more important than the code itself.” It’s not an exaggeration. Tests give us 2 things:
    • It gives us guardrails when we make change. Having sufficient test coverage will make any refactor work a lot easier, so that we can tolerate suboptimal code(to a degree).
    • Combined with the readability above, it also serves as living documents of the software behavior, which helps us understand the code.

How to Step Up

From stage 3 to stage 4 is a mindset change. There’s no new technique to learn.

Stage 4: Code is not the Product. It’s a Liability

What’s a job well doneOptimize lead time for long term
(lead time = the time span from a business problem is identified to a solution is released to prod)
Mindset – Software Engineer’s job is not to write code, but to provide solution.
– Less code means faster delivery and less coupling for future changes.
PrincipalsMVP

Stage 3 afaik is the best we could do on the programming part of software development. However that’s not the entire job as a Software Engineer. A software engineer’s job is not to write code, but to provide solution to business questions. Writing code is the main method to achieve the goal, but not the goal itself.

Software Engineers are closest to the current implementation of the software, and are (usually) the most sensitive to emerging technologies. This means we are in the best position to come up with solutions. By utilizing existing functionality / new technology, we can find ways to deliver more with less effort. When Software Engineers drives solutions, amazing things happen:

  1. We get to deliver better software with less upfront effort. This is what I mean by “Code is not the Product”, we’re aiming to solve a problem, not to deliver code.
  2. We don’t add unnecessary complexity into the codebase, and by doing that we achieve higher efficiency for future ourselves. This is what I mean by “Code is a Liability”. Every line of code is a burden to maintain in the future.

Engineers should work alongside Product Managers to explore the minimum feature set that could solve the business problem at hand. Software Engineers excel at discovering efficient paths, yet these shortcuts can sometimes have drawbacks – certain features may need to be sacrificed to achieve this efficiency. The engineer’s responsibility is to highlight potential shortcuts, engaging in negotiations with PMs and UX designers to arrive at a solution that is cost-effective without compromising the business objectives.

You might question “why cheapest”, since our instinct is cheap products are often low quality. While the statement may be true for kitchen appliance, longivity is not a virtue of a software codebase. A software product is constantly evolving. Our aim is to provide potentially valuable feature to the customer, test the market feedback as early as possible, then come back to evolve the feature(aka Experimental Mindset). The solution must be designed around achieving these objectives and nothing more.

It is worth emphasizing that “low code solution” does not mean “hacky solution”. Hacky solution here means low quality code, which although seems to be enabling fast turnover, will jeopardize long term delivery speed. For me, a definitive validation would be “whether the changed code still fit our domain model”.

By:


Leave a comment