You should always be “Open to Work”

In 2022, one of the best senior Java developers I knew reached out to me about leaving his current company—where he had been for a decade—and asked if I knew anyone hiring. Instantly, I let the recruitment team where I work know and got him into the process to work with us. I had worked with him before, I had seen him on stage at events, and I knew he had the right personality to make a big impact on the business. I knew he would get hired. Except he failed the coding interview.

I was genuinely shocked—so shocked that I did something I had never done in 20 years of work: I assumed the interviewers were wrong and escalated so he could get a second chance. And he did get a second chance—and wouldn’t you know it, I was proven to be a fool because he failed the coding interview again—it was the same question too!

I spoke to my friend about it, read the interviewer’s notes, and concluded that despite everything I knew about him, the skill he lacked was the ability to interview. We were his first (and second) interviews in a decade, and he just handled them the way one would handle a meeting.

As I am a fool, I should’ve expected this, since I’ve held a belief since 2013: always be open to an interview. You can love your job, but if you get an offer to interview—take it.

I’ve lost track of how many interviews I’ve done in the 11 years since this realization, and I’ve experienced the full range—from rejection to offer letters. In that time, I’ve only taken two roles.

It sounds like a waste of time to do hundreds of interviews and get nothing for it, even if you love your job. But I don’t think it is. First, as with my friend, interviewing is a skill—and like any skill, it needs to be practiced and kept up to date. This includes not being stressed, and doing more interviews helps with that, which leads to a better understanding of what’s being asked and allows you to demonstrate your skills better.

In tech, especially, the language we use and the tools change often, and being able to speak to the state-of-the-art is important—especially if you’ve been in a company working on one tech stack or architectural design for a long time. Equally, as you grow, you need to know what questions will be asked of you so you can practice your answers and find the anecdotes and examples to share to show your experience.

The second reason is market data—because like anything, it’s only worth what someone will pay for it. Interviewing gives you real-world data on how what you earn today compares to the market. The same goes for demand: you can see how many roles exist for your preferred skill or level.

When it comes to salary discussions in your own organization, being armed with real-world data will help you have realistic expectations.

The third reason? Improving your own company’s hiring process. I can’t explain how many terrible interview processes I’ve seen—and it totally puts me off working with that company. But I’ve also seen many good ones. I’ve taken those ideas and shared them where I work, allowing the companies I’ve worked with to develop amazing interview processes.

This isn’t without risk—people you work with might jump to incorrect conclusions about your happiness if they hear you’re interviewing. To solve this, I’ve always been open with my managers about interviewing and told them they’d never be blindsided if I left. If I ever found something better, I committed to discussing it with them ahead of time.

Some of my best managers fully bought into it, and I think that speaks volumes to their confidence in providing a great work environment and building trust with their teams.

So, in summary, I encourage you to get your CV on OfferZen, set yourself to “Open to Work” on LinkedIn, and sharpen your interview skills—just tell your manager first.


Thoughts on Rust

I finally caved into the pressure from all the smart people I know who have told me for at least the last two years that the Rust programming language is wonderful and we should all jump on it. I finally caved and decided to follow some tutorials over a week and then attempted to build a small program with it. If you do not know my background, I have never coded in C/C++—the closest I did was Turbo Pascal, and later Delphi—but I have mostly worked with memory-managed runtimes for 15 years (.NET, JVM, JS). So caring deeply about memory is a big change for me.

For interest, I did all my coding either in the Rust playground (which is nice) or VSCode, which worked flawlessly.

Before I get to my thoughts on it, I want to call out five interesting aspects that were top of mind for me.

Included tooling

The first thing that jumped out was the lovely tooling, called Cargo, which is included as standard. In theory, you could avoid it and just use the pure Rust language, but literally every tutorial and discussion assumes it is there—and it makes life so much easier. It is beautiful and handy to have a universal tool stack. Cargo does several things: at first, it is a capable package manager, similar to Node or Yarn. In addition, it includes a built-in formatting tool and a linter, which helps reduce bikeshedding around language conventions. This is wonderful. Rust and Cargo also have their own testing framework, lowering barriers and making it so easy to get started. Cargo even includes a documentation generator that takes comments from your code—combined with the code itself—to build very useful docs.

Delightful error messages

When I made mistakes in my code—which happened often as I got to grips with the language—the error messages Rust produced were absolutely beautiful and useful! For example, I tried to use ++ (to do an increment), which does not exist in Rust, and the error message very clearly told me it does not exist and what I could use as an alternative instead.

Standard Error Compiling playground v0.0.1 (/playground)
error: Rust has no postfix increment operator
--> src/main.rs:3:10
   |
3  | hello++;
   |      ^^ not a valid postfix operator
   |
help: use `+= 1` instead
   |
3  | { let tmp = hello; hello += 1; tmp };
   | +++++++++++ ~~~~~~~~~~~~~~~~~~~
3 - hello++;
3 + hello += 1;

Documentation unit tests

Having "built-in" tooling—especially for testing—means you can do amazing things with the knowledge that it is there. Rust does one of the most interesting things I've seen in any language: including unit tests in the documentation. These tests are executable in VSCode and run with Cargo, but they don’t need to be far from the code—it’s awesome. They also end up visible in the documentation that Cargo can generate. It’s brilliant.

Shadowing immutable variables & redeclaring variables

Moving from the good to the bad: redeclaring variables is something Rust supports, and it baffles me that it is even allowed. A small primer on this: variables are immutable by default in Rust—this is great! You can make them mutable if you want, and changing the value of immutable variables is not possible. For example, this fails:

fn main() {
    let aMessage = "This is the initial message";
    aMessage = "this will error"; // Error: cannot assign twice to immutable variable
    println!("{}", aMessage);
}

But we can redeclare the variable—even if it was previously immutable—and I understand some scenarios where this might be useful, but it feels dirty. If this were limited to mutable variables, it would make more sense, but it’s confusing. In the previous example, it prints "this will error," which is at least logical. However, when we add changes to scope, everything goes out the window. It feels like a mess to me, and I hope the linter grows stricter to discourage it.

Passing ownership of variables to functions

This isn’t bad, but it’s easily the biggest shift in thinking required. If you’re coming from C#/Java/JS and you create a variable inside a function and pass it to another function, that variable still exists in the original scope. In Rust, unless you opt-in—and meet other requirements—if you pass a variable to a function, that function owns it, and it ceases to exist in the original scope. Here’s an example of what I mean:

fn writeln(value: String) {}

fn main() {
    let a_message = String::from("Hello World");
    println!("{}", a_message);
    writeln(a_message);
    println!("{}", a_message); // Error: value borrowed here after move
}

It’s confusing at first, but there are solutions, and I do like that Rust encourages better design patterns through this rule.


Build a Google Calendar Link

I recently created an event website and needed to create links for people to add the events to their calendars—the documentation for how to do this for Google Cloud is a mess, so here’s what I eventually worked out.

Start with: https://www.google.com/calendar/render?action=TEMPLATE

Now, each piece we’ll add is an additional parameter:

  1. Title is specified as text, so append that to the URL if you want that. For example, if I wanted the title to be Welcome, I would add &text=Welcome, e.g.: https://www.google.com/calendar/render?action=TEMPLATE&text=Welcome

  2. Date/Time is next, starting with the keyword dates. Here, we specify the date as YYYYMMDD (e.g., 1 July 2022 is 20220701), and times are formatted as HHMMSS (e.g., 8:30 AM is 083000). The date and time are separated with the letter T, and the start and end pieces are separated with /. For example, if the Welcome event starts at 8:30 AM on 1 July 2022 and ends at 10 AM, the value would be: 20220701T083000/20220701T100000

  3. Timezone is optional—without it, you get GMT. If you want to specify a timezone, use ctz, and the value is a tz database entry. For example, if you want South Africa, it would be: Africa/Johannesburg

  4. Location is also optional and uses the key location with free-form text. If we put the above example together, we get:

    https://www.google.com/calendar/render?action=TEMPLATE&text=Welcome&dates=20220701T083000/20220701T100000&ctz=Africa/Johannesburg&location=Boardroom
    

Notes:

  • You must URL encode the values you use. For example, if you had a title of Welcome Drinks, it would need to be Welcome%20Drinks.
  • There are other parameters (e.g., for descriptions) that I never used, so I don’t have them documented anymore.

How do you, and other companies, handle tech debt?

I was asked the question a while ago, How do I handle tech debt? and I am not sure I ever put it down in a form that makes sense; so this is an attempt at trying to convey several tools I use. ## Pay back early

The initial thinking on handling tech debt is not my idea, but stolen from Steve McConnell’s wonderful Code Complete. Steve shows in his book that if you can tackle tech debt earlier in the lifecycle of a project, the cost of tech debt is a lot less. Basically, the quicker you get back to repaying that debt, the cheaper it will be.

One aspect to keep in mind is that while the early part of a project may refer to greenfields/new projects, it is not limited to that idea. The early part could also mean epic-level-sized pieces of work that are started on an existing project, thus the "catch it early and it is cheaper" principle applies to existing teams just as much as it does to teams starting a new project.

Organisation

When I think of what to do specifically to handle tech debt, the first—and only—solution that won’t fit easily into an existing team is team organisation. I’ve seen this at Equal Experts, and previously when I worked at both AWS and Microsoft.

The simple answer is: teams above 10 people fail. Why is 10 the magic number? There are two key reasons:

  1. Psychology of collaborationDunbar’s number suggests 15 is the upper limit for meaningful close relationships. However, since teams need to foster relationships across an organisation—not just within their own team—the optimal size is capped at ~10.

  2. Cognitive load – As systems grow increasingly complex, holding all necessary information in one’s mind becomes harder. Keeping teams small (~10 people) naturally limits the volume and complexity of work they can handle, resulting in many focused, self-contained teams.

When I say team, I mean the entire team: POs, QAs, BAs, and any other two-letter-acronym roles. The whole unit should stay at or below 10—so you might have just 4 engineers per team.

Cross-skilled teams

There’s value in teams of 8–10 people who collectively hold most of the skills needed to deliver end-to-end results. Specialised teams—like dedicated front-end or back-end groups—should be rare. Most teams should own features from start to finish, fostering collaboration and shared accountability.

This approach forces teams to:

  • Work together toward deadlines and deliverables
  • Push back on new work if their existing debt risks overwhelming them
  • Become experts in their technology stack (reducing generalist trade-offs and lowering tech debt over time)

DevOps

I always encourage clients to adopt the DevOps mindset. The cross-skilled, end-to-end ownership model hints at this, but one of DevOps’ earliest pillars—"You build it, you run it"—is critical.

This philosophy means a team owns a feature’s full lifecycle: writing, deploying, monitoring, and supporting it. At first glance, this might seem impossible—how can a small team handle everything? But more on that later.

DevOps’ impact on tech debt: While tech debt rarely causes outages, recovery time often worsens due to it. A major outage’s fallout includes post-incident reviews, client impact assessments, and root-cause analysis. Nothing motivates a product owner or team to reduce debt faster than the risk of being woken at 2 AM for incidents—and spending hours fixing them.

Happy to share a client video on this very topic from my latest project.

Reign in tech

In large organisations, limiting technology choices reduces tech debt. Outdated or obscure systems create more debt than stable, well-supported tools.

Avoid bleeding-edge tech

Bleeding-edge solutions build debt faster and hurt more when maintenance becomes necessary.

Horizontal scaling teams

The cost of starting and frustration with monolithic systems can deter investment—another debt risk. A solution? Build horizontally focused teams that own single features or platforms other teams rely on.

Examples:

  • IDPs (Identity Providers)
  • Teams handling web routing, bot detection, caching, etc.

These teams set standards (e.g., "We support React ESIs for caching"), letting business teams adopt them for speed/support. If they choose alternatives, they must justify trade-offs (e.g., lost speed).

Equal Experts’ Digital Platforms playbook covers this well.

Trickle-down updates

Horizontally scaled teams naturally force dependencies to update. For example, when the deployment-pipeline runners team upgraded, we had to migrate—cleaning old systems in the process. The short-term pain led to a more robust setup.

Bar raisers

At AWS, bar raisers were a standout tool. Unlike gatekeepers, they provided guidance without blocking progress. For high-risk deployments, you’d submit a form outlining risks, tests, and recovery plans. Bar raisers reviewed it, offering feedback—not vetoes.

Why it worked:

  • No micromanagement
  • Encouraged knowledge sharing
  • Prevented teams from repeating past mistakes

They operated on set schedules, preventing overload.

Tech debt is normal work

The first easy fix? Treat tech debt like regular work—log it in your backlog. Neglecting it drags down average ticket times and frustrates teams. Product owners prioritise better when they see its impact.

Teams practicing "you build it, you run it" naturally face ebbs and flows (e.g., quiet periods during holidays). Quiet time? Perfect for debt reduction.

Finally: you can’t fix what you can’t see. Document debt explicitly—even if it’s worse than expected—and act on it.

Tech debt sprints

The last idea comes from Microsoft: add a tech-debt sprint at the end of every feature. Ship MVPs fast, gather feedback, and pile up debt—then fix it. Teams know it’ll be addressed, boosting morale.

I spoke about this at Agile Africa if you’re curious.


Keeping dependencies up to date

If you work with JavaScript or TypeScript today, you have a package.json with all your dependencies in it, and the same is true for the JVM with build.gradle. In fact, every framework has this package management system, and you can easily use it to keep your dependencies up to date.

In my role, every time I add a new feature or fix a bug, I update those dependencies to keep the system alive. This pattern originates from my belief that part of being a good programmer means following the Boy Scout Rule. I was recently asked if I believe that these dependency upgrades are risky and whether we should batch them up and do them later, since that would make code reviews smaller and our code less likely to break from a dependency change.

I disagreed—but saying "the Boy Scout Rule" isn’t enough of a reason to disagree. That’s just a way of working. The reasons I disagreed are:


Versions & Volume

All dependency version upgrades carry the risk of failure—meaning they break our code in unexpected ways. There’s a standard that minor version changes should be safe to upgrade, which is why I often do them all at once with minimal checks. Major version changes, however, require more care and understanding.

Major changes typically happen naturally. That’s because a major version update is how dependency developers signal, "Hey, there’s something you should know." But relying on "major vs. minor" as hard rules is flawed. These distinctions are more like guidelines—signposts for how to approach the situation. Much like when you drive a car, a change in road speed is a signal to adjust your caution accordingly.

For example, even type version changes and the volume of changes aren’t absolute factors. Let me illustrate this with last week’s experience: I performed two minor version updates on a backend system as part of a normal feature addition. It broke the testing tools because one of the dependencies had a breaking change—a minor version with a breaking change. This was human error on the part of the dependency’s developer: they released a minor version but didn’t mark it as a major change. That mistake impacted how I approached the updates—and it always increases the chance of issues.

Software is built by humans. Humans, not versions, will always be the source of errors.


Risk & Reward

I do like the word "risk" when discussing whether to update, because risk never exists in a vacuum—it always comes with a reward. How often have you heard people say, "Updating is too risky"—focusing solely on the chance of something breaking—without mentioning the reward of updating?

Stability is not a reward; stability is what customers expect as the baseline.

When we do update, we gain code that performs better, costs less to maintain, and is more secure. The discussion isn’t "What will break?" but "Why wouldn’t we want faster, safer, and cheaper code?"

I’ve inherited code from a team that never updated dependencies. It’s packed with outdated versions—a high chance of breaking as we start upgrading. But contrast that with projects my team builds, where we update dependencies every time we make a change. We’re only ever dealing with one or two small updates at a time. When issues appear, they’re easy to spot and fix.

Death, taxes, and having to update your code. As a developer, the only way to avoid updating your code is to pass it to someone else or change teams. Eventually, you’ll need to upgrade—and doing it often, in small batches, is cheaper and easier for you.

Using the backend example again: I had only two small changes to dependencies, so my issue was isolated to one of them. I could quickly check both and found the release notes for one within 15 minutes—clearly documenting the logic change. That let me adjust the code and keep us on the new version. If I had 100 changes? I would’ve rolled everything back and gone to lunch, and future me would have hated past me for it.


Architects & Gardeners

Our job isn’t to build a stable monument and abandon it. I believe deeply in DevOps, and thus in the truth that software is evolutionary—it needs care.

We are gardeners of living software, not architects of software towers. In our world, when things stop… they’re dead.

Maintenance and fixing what breaks is core to our belief that the best way to deliver value is through living software.


Tenets of stable coding

  1. Build for sustainability We embrace proven technology and architectures. This ensures that the system can be operated by a wide range of people, and experience can be shared.
  2. Code is a liability We use 3rd-party libraries to lower the code we directly need to create. This helps us move fast and focus on the aspects that deliver value to the business.
  3. Numbers are not valuable by themselves; we focus on meaningful goals and use numbers to help our understanding. We do not believe in 100% code coverage as a valuable measure.
  4. We value fast development locally and a stable pipeline. We should be able to run everything locally, with stubs/mocks, if needed. We use extensive git push hooks to prevent pipeline issues.
  5. We value documentation—not just the "what", but also the "why".
  6. We avoid bike shedding by using tools built by experts to ensure common understanding.

We acknowledge that there are the physics of software which we cannot change

  1. Software is not magic.
  2. Software is never “done.”
  3. Software is a team effort; nobody can do it all.
  4. Design isn’t how something looks; it is how it works.
  5. Security is everyone’s responsibility.
  6. Feature size doesn’t predict developer time.
  7. Greatness comes from thousands of small improvements.
  8. Technical debt is bad but unavoidable.
  9. Software doesn’t run itself.
  10. Complex systems need DevOps to run well.

From Tom Limoncelli; his post goes into great detail.



OWASP TOP 10

Recently been talking a lot about the OWASP Top 10 and have created some slides and a 90-minute talk on it! So if you want to raise your security awareness, this is a great place to start.


You are blocked

For the last 4 months I have taken what could be seen as extreme—blocking a few hundred thousand people on Twitter. This has led, in the last week, to a few people asking or pointing out that they are blocked and wondering why I chose them. The reality is, I probably did not block them. I blocked someone else (let’s call them the aggressor), and using a tool I wrote, I block the aggressor and all their followers. So while I’ve actively blocked a few hundred aggressors, it balloons to hundreds of thousands of followers.

Why?

Twitter, for me, is a place I go to hang out with people I like—to learn from them and hear their stories. Twitter, for me, is my cocktail party; except it is getting gatecrashed by the alt-right, racism, sexism, and nationalists. People I do not want at my cocktail party. It has caused me to have too many shitty experiences, so I now have a bouncer.

I have taken the view that if someone follows a problematic person, the chance that they are problematic themselves is higher than I am willing to risk. It is not consistent, actually—the larger the account, the less likely it is to be accurate. Donald Trump is a perfect example: so many people follow him for so many reasons that while I do not agree with Trump, to block all his followers is not going to work.

Still Visible

Twitter’s block is lovely in that you can still view my thoughts—just open a private tab in your browser. It merely makes the cost to interact with me higher, which is ideal. Increasing the cost means that either someone needs to be determined to be a jerk (easier to manage), or they really value the interaction. I can handle those edge cases easier.

Private

Why not make my account private? I tried this, but it makes engagement impossible—it is not a good long-term plan.

Bubbles

The "engage me" crowd like to say blocking creates echo chambers where you cannot learn anything, and I agree if someone does this everywhere—but Twitter is merely one of many places I engage and learn. I choose to have Twitter be fun and a cocktail party. I choose other places to learn. You may have different views on how you use Twitter, and that is great for you. We will agree to disagree.

Unblock

Want to be unblocked? Drop me an email or get someone I know to DM me and check who you follow. Since you are influenced by the people you spend time with, don’t spend time with horrible people.