Game is done…kinda”

Where we are?

https://depoco.itch.io/holy-carp

Ok, so the game is mostly done. I’ve uploaded it to itch.io. It’s still missing a few elements, but I’ll be adding more as I go.

This is only my second game ever, and honestly, I’m pretty happy with how it turned out. Of course, I wanted it to have tons of features and amazing gameplay, but those will come, step by step.

In this post, I’ll dive deeper into my GameManager and some refactoring I did, the good stuff that keeps the project tidy and manageable.

What needs to be added

Some of the pixel sprites needs to be fixed, it seems that they got messed up when I imported it into Godot, no idea why.

Sort out the menu and make it look prettier and at the same time add the audio I have + the settings window.

GameManager stuff

So this is my final script for the GameMananger:

extends Node

signal lives_changed(new_lives)
signal score_changed(new_score)


var lives = 3
var current_score = 0

# KEEPING SCORE ---------------------------------

func _update_score(fish_score):
  current_score += fish_score
  score_changed.emit(current_score)

#------------------------------------------------

# HEALTH ----------------------------------------

func _decrease_life():
  if lives > 0:
    lives -=1
    lives_changed.emit(lives)

  if lives == 0:
    _game_over()

#------------------------------------------------

# GAME UI STUFF ---------------------------------

func _game_over():
  var game_over_scene = preload("uid://behejrhw6wlo7")

  var game_over_instance = game_over_scene.instantiate()
  var GameOverCanvas = get_node("/root/MainScene/GameOverCanvas")

  GameOverCanvas.add_child(game_over_instance)

  get_tree().paused = true

#------------------------------------------------

func reset_game():
  lives = 3
  current_score = 0
  lives_changed.emit(lives)
  score_changed.emit(current_score)

It keeps track of score, lives and game over.

This is just a quick update. I’m starting a new game and have a lot on my plate right now, so progress on this post will be occasional.

I’ll be adding more to it from time to time. Also, keep an eye out for new posts about my new game. I plan to release Part 1 next week.

My dear readers. Remember that everything I write is not always 100% correct. I do mistakes and I’m a novice at this. So if you find a mistake or just have some random feedback on anything. Please comment!

The Engineering Manager as Coach, Not Boss

I want you to think about the best manager you have ever had. Not the most technically impressive one, not the one who shipped the most features, but the one who made you genuinely better at your job. I know who that person is for me, and I bet you know who that person is for you too.

Odds are good that person did not micromanage your work. They probably did not hover over your PRs or tell you exactly how to solve every problem you brought to them. What they likely did was ask you questions that forced you to think more clearly. They gave you feedback that was specific enough to actually act on. They took your career seriously in a way that felt genuine rather than performative. They helped you see the gap between where you were and where you were capable of getting, and then they helped you close it.

That is coaching. And it is the most underused and underdeveloped skill in engineering management.

Most engineering managers are never taught how to coach. They come up through the IC track where performance is individual, then they get promoted and suddenly they are responsible for other people’s growth without any real framework for how to do it. The instinct that fills the gap is usually managing – telling people what to do, reviewing their work, tracking their progress – rather than coaching – helping people develop the capacity to figure out what to do themselves.

That distinction matters more than it might sound. A team of engineers who are managed well will execute against a clear plan competently. A team of engineers who are coached well will grow, take on more complex work, make better decisions independently, and stay longer because they feel like they are actually developing as professionals. The output difference between those two teams compounds significantly over time.

So let’s get into what coaching actually looks like in the day-to-day context of engineering management.

The one-on-one is your primary coaching surface. Not a status meeting. Not a task review. The one-on-one is where you develop your understanding of what this person is working on, what is getting in their way, what they are learning, and what they are ready for next. Done well, it is where the most important growth conversations happen.

Most one-on-ones fail because they do not have a clear purpose and the manager ends up driving the conversation around whatever is top of mind for them – team updates, upcoming deadlines, process concerns. Flip that. The one-on-one belongs to the engineer. Open with something like: “what is on your mind this week?” or “what do you most want to talk through today?” Then actually follow their lead.

What you are listening for is not just the content but the subtext. An engineer who says “this sprint has been a bit slow” might be telling you they are bored. An engineer who says “I have just been heads down in the ticket queue” might be telling you they feel disconnected from meaningful work. An engineer who mentions a difficult interaction with a product manager three times across different conversations is probably telling you something important without saying it directly. Coaching requires learning to hear those signals and respond to them honestly rather than staying on the surface.

Questions are your most important tool as a coach. The instinct for most technically strong managers is to give answers. Someone comes with a problem and you tell them how to solve it. That feels productive and it often is in the short term. But it creates dependency. Every time you give someone an answer you had the chance to help them develop the capability to generate that answer themselves, and you declined it.

The shift is to respond to problems with questions. Not in a frustrating Socratic way where you refuse to ever be direct, but in a way that draws out the engineer’s own reasoning before you add yours. “What have you already tried?” “What do you think is causing it?” “If you had to guess at the best path forward, what would you say?” These questions accomplish two things simultaneously: they give you information about how the engineer is thinking, and they often help the engineer arrive at the answer themselves, which is a much more durable outcome than being told the answer.

When you do share your perspective, frame it as a perspective rather than a directive. “Here is how I would think about this” rather than “here is what you should do.” That keeps the ownership with the engineer and signals that their judgment is part of the equation, not just an obstacle between them and the right answer.

Performance conversations are where a lot of engineering managers lose their nerve, and that avoidance does real damage. When someone’s performance is not where it needs to be, the kindest thing you can do is tell them clearly and specifically, with enough context that they can actually act on it. Delayed feedback is not compassion. It is conflict avoidance dressed up as patience, and it ends up being far harder on the engineer in the long run.

The structure that works for performance conversations, whether they are corrective or developmental, follows a consistent pattern. Specific observation first – not “your code quality has been slipping” but “the last three PRs you submitted had significant issues that were caught in review and would have caused problems if they had gone to production.” Then impact – why this matters for the team, the product, or the engineer’s own growth. Then genuine curiosity – what is going on from their side, what might be contributing to this, what do they think they need. And then a clear shared understanding of what improvement looks like and a timeline for revisiting it.

The specific observation part is what most managers skip or soften into meaninglessness. “I just want to flag that some people have mentioned concerns about collaboration” is not actionable feedback. “In the incident last week, two engineers told me they felt their concerns were dismissed when they raised them in the thread – I want to understand what happened and work through it with you” is actionable feedback. Specificity is the thing that makes feedback usable rather than just uncomfortable.

Growth plans are one of the most underutilised management tools I know. Not the formal HR document kind that gets filled out once a year and then filed – the real kind, where you and an engineer have an honest conversation about where they want to go, what is in the way, and what you are both going to do about it.

A useful growth plan starts with a genuine conversation about what the engineer actually wants. Not what you think they should want, not what the standard career ladder says they should be aiming for. What do they actually want? More technical depth? Leadership experience? Exposure to a different part of the stack? The answer to that question changes what kinds of opportunities are meaningful to them and what kinds of stretch assignments will actually develop them rather than just adding to their workload.
From there, you build backward. If someone wants to move toward a staff-level role, what does the gap between where they are now and what that role requires actually look like? What specific skills, what visibility, what demonstrated impact would they need to show? Get specific enough that both of you could look at a piece of work and agree on whether it counts as progress toward the goal.
Then identify the opportunities. Not abstract goals – specific opportunities in the actual work ahead. This project coming up would give them the technical leadership visibility they need. This initiative has a coordination component that would develop their cross-team communication. This piece of the codebase is complex enough that owning it would give them the depth that is currently missing from their profile. When you can connect growth goals to real work, growth planning stops being a separate activity and becomes part of how you think about the team’s work anyway.
Revisit the plan regularly. Not in the annual performance review – in one-on-ones, every few weeks. “How is the work on that service going – is it giving you what you were hoping for?” That regularity signals that you are actually invested in the plan rather than using it as a box-checking exercise.

                      Career development conversations are different from growth plans in a specific way: they require you to engage with the engineer's ambitions beyond their current role, and sometimes beyond your team. This is uncomfortable for some managers because it feels like you are helping someone leave. In reality, you are building the kind of trust and loyalty that makes people want to stay.

The engineers who leave most readily are the ones who feel invisible – whose manager has never asked where they want to go, who cannot see a path forward from where they are. The engineers who stay in companies that would otherwise lose them are often the ones who have a manager who takes their development seriously enough to have honest conversations about what they want, even when the answer is complicated.

If an engineer tells you they want to move into management, help them explore that seriously rather than steering them back toward IC work because you need their technical contribution. If someone tells you they want to work on a different kind of problem than your team addresses, help them think through what that could look like internally before they start looking externally. That kind of engagement builds a relationship where the engineer feels like their interests are actually considered, not just tolerated.

Feedback culture within a team starts with how the manager gives feedback, but it does not stop there. If you want engineers to give each other honest, constructive feedback – in code review, in design discussions, in retrospectives – you need to model it and you need to make it safe. That means appreciating directness when you see it even when it is directed at you, and explicitly naming feedback behaviours you want to see more of when they show up.

One habit that is underrated: giving positive feedback with the same specificity you bring to corrective feedback. “Good job on that feature” lands with almost no impact because it tells the engineer nothing about what specifically was good. “The way you handled the rollback plan on that deploy was exactly the kind of thing I want to see more of – you anticipated the failure mode before it happened rather than waiting for it” – that lands. It tells the engineer something specific about their performance and it communicates something about your values. Both of those things are useful.

The gap between a manager who manages and a manager who coaches is ultimately a gap in how they think about their job. A manager who manages thinks their job is to get work done through their team. A manager who coaches thinks their job is to develop people who can get increasingly complex and important work done without needing to rely on the manager as a decision-making crutch.

The first version of that job has a ceiling. The second version compounds.
Engineers who work for coaches become more capable over time. They take on bigger problems, mentor more junior colleagues, contribute to the team in ways that go beyond their individual output. They are also, in my experience, far more likely to speak honestly with you about what is working and what is not – because the coaching relationship is built on genuine engagement rather than authority, and that changes what people are willing to say.
That honesty, in the end, is one of the more valuable things you can build. Because you can only fix the problems you know about.

Next in the series: Technical Debt – When to Fix, When to Ship. A framework for making trade-off decisions and communicating them to stakeholders who care about outcomes, not architecture.

How to Find a User’s Rank Based on Points Using Django ORM

If you’re building something like a leaderboard, a fitness tracker, or any competitive feature, ranking users by their score is something you’ll run into pretty quickly. It sounds simple — “just sort them and pick the position” — but when you get into Django ORM, there are actually a few different ways to do this, and they’re not all equal. Let me walk you through all of them, explain how each one works, and tell you which one I’d actually use in production.

The Setup

Let’s say you have a User model with an id, name, and points field. Something like this:

id name points
1 Alice 150
2 Bob 200
3 Carol 100

Bob has the highest points, so he should be ranked 1st. Alice is 2nd, Carol is 3rd. Simple enough in concept — now let’s look at the different ways to get there in code.

Approach 1: Window Functions (The Proper Way)

Django has had support for window functions since version 2.0, and if you’re not using them, you’re missing out. This is the cleanest, most scalable solution.

from django.db.models import F, Window
from django.db.models.functions import Rank

users_with_rank = User.objects.annotate(
    rank=Window(
        expression=Rank(),
        order_by=F('points').desc()
    )
)

# Get Bob's rank
bob_rank = users_with_rank.get(id=2).rank
print(f"Bob's Rank: {bob_rank}")  # Bob's Rank: 1

This runs a single SQL query and calculates the rank for every user in one shot. The Window class tells Django to use a SQL window function, Rank() handles the ranking logic, and order_by=F('points').desc() makes sure the person with the most points gets rank 1.

One thing to know about Rank(): it handles ties the same way SQL’s RANK() function does — if two users have the same points, they get the same rank, and the next rank is skipped. So if Alice and Bob both had 200 points, they’d both be rank 1, and Carol would jump to rank 3 (no rank 2).

If you don’t want that gap, use DenseRank() instead:

from django.db.models.functions import DenseRank

users_with_rank = User.objects.annotate(
    rank=Window(
        expression=DenseRank(),
        order_by=F('points').desc()
    )
)

With DenseRank(), tied users both get rank 1, and the next user gets rank 2 — no skipped numbers.

Approach 2: Subquery / Count Filter (Quick and Simple)

This one is easy to understand and works without any special imports. You count how many users have more points than the user you’re looking at, then add 1.

specific_user = User.objects.get(id=2)  # Bob
user_rank = User.objects.filter(points__gt=specific_user.points).count() + 1
print(f"Bob's Rank: {user_rank}")  # Bob's Rank: 1

This makes two queries — one to fetch Bob, one to count users above him. For a small table, that’s totally fine. The problem is it doesn’t scale well. If you need ranks for 50 users, you’d run 51 queries. And it doesn’t gracefully handle ties — if two users are tied, they’ll both show different ranks depending on who you ask about first.

Use this approach when you need the rank of a single user on a small dataset and you want to keep the code dead simple.

Approach 3: Raw SQL

Sometimes you just want to drop into raw SQL and be done with it. Django lets you do that with raw():

users = User.objects.raw("""
    SELECT id, name, points,
           RANK() OVER (ORDER BY points DESC) AS rank
    FROM myapp_user
""")

for user in users:
    print(f"{user.name}: Rank {user.rank}")

This works, and it gives you full SQL flexibility. But you lose all the ORM benefits — no composability with other querysets, no easy filtering afterward, and you have to manually handle the table name. I’d only reach for this if the ORM genuinely can’t express what you need, which is rare.

Approach 4: Python-Side Sorting

This one’s not a database approach at all — you pull everyone into memory and sort in Python:

all_users = list(User.objects.order_by('-points'))
for index, user in enumerate(all_users, start=1):
    user.computed_rank = index

# Find Bob
bob = next(u for u in all_users if u.id == 2)
print(f"Bob's Rank: {bob.computed_rank}")  # Bob's Rank: 1

This is fine if you’re already loading all users for other reasons (say, rendering a full leaderboard page). But if you’re just doing this to find one person’s rank, pulling the entire table into Python memory is wasteful. It also doesn’t handle ties — everyone gets a unique rank even if scores are equal.

Approach 5: Caching the Rank (For High-Traffic Apps)

If your leaderboard gets hit thousands of times a minute and the scores don’t change that often, computing the rank on every request is unnecessary load on your database. A common pattern is to compute ranks periodically and cache them:

from django.core.cache import cache
from django.db.models import F, Window
from django.db.models.functions import Rank

def get_cached_rankings():
    cached = cache.get('user_rankings')
    if cached:
        return cached

    users = User.objects.annotate(
        rank=Window(
            expression=Rank(),
            order_by=F('points').desc()
        )
    ).values('id', 'rank')

    rankings = {u['id']: u['rank'] for u in users}
    cache.set('user_rankings', rankings, timeout=300)  # Cache for 5 minutes
    return rankings

def get_user_rank(user_id):
    rankings = get_cached_rankings()
    return rankings.get(user_id)

This way, the expensive window function query only runs once every 5 minutes (or whatever interval makes sense for your app). Every other request just reads from cache. You can trigger a cache invalidation whenever a user’s score changes if you need it to be more real-time.

Which Approach Should You Use?

Here’s a quick breakdown:

Approach Best For Watch Out For
Window Functions Most cases — clean, scalable, handles ties properly Requires Django 2.0+ (not an issue in practice today)
Count Filter Single user rank, small dataset, simple code Multiple queries, doesn’t handle ties well
Raw SQL Complex ranking logic the ORM can’t express No ORM composability, manual table names
Python Sorting Already loading full list for other reasons Memory usage, doesn’t handle ties
Caching High-traffic apps with infrequent score changes Stale data between cache refreshes

Honestly, for 90% of use cases, window functions are the right answer. They run a single efficient query, the database handles all the logic, ties are handled correctly, and the code is readable. The only time I’d deviate is if I need real-time ranks on a very high-traffic endpoint — in that case, I’d combine window functions with caching.

The count filter approach is fine for a quick prototype or a low-traffic admin page. Raw SQL is a last resort. Python sorting is situational. And caching is a performance optimization you layer on top of whichever database approach you pick.

Wrapping Up

Ranking users in Django isn’t complicated once you know your options. Start with window functions — they’re what the database is built for and Django exposes them cleanly. If you run into performance issues at scale, add caching on top. And if you’re prototyping quickly, the count filter approach will get you there in two lines.

The main thing to decide early is how you want to handle ties, because that choice shapes which function you reach for. Get that right upfront and the rest is straightforward.

Strategy vs. Execution: How Leaders Set Technical Vision

There is a version of technical leadership that looks like this: you are deep in a sprint, your team is shipping, the product roadmap is clear, and everyone knows what they are building. Everything is humming. You feel productive. You feel useful.

And then six months later you look up and realise you have been executing against a direction that no longer makes sense. The architectural decisions made eighteen months ago are now actively fighting the product requirements coming in today. The platform you are running on does not support the scale you need. The technical debt you kept deferring is now the reason your velocity has halved.

This is what happens when execution runs ahead of strategy. And in my experience, it is one of the most common failure modes in technical leadership, because the short-term feedback loop for execution is so much tighter and more satisfying than the long-term feedback loop for strategy.

Execution tells you immediately whether it worked. Strategy takes months or years to validate, and by then the leader who made the call may not even be around to learn from it. That asymmetry creates a natural gravitational pull toward doing over thinking, shipping over planning, tactical clarity over strategic ambiguity. Understanding that pull is the first step toward resisting it when you need to.

This article is about the work that sits above execution: how senior engineering leaders set technical direction, how you align that direction with product and business goals, how you communicate it to the people building against it, and how you maintain it over time as circumstances change.

Setting technical vision starts with a question that sounds simple but is actually quite hard to answer well: what problem is this engineering organisation trying to solve? Not “ship features quickly” — that is a capability, not a purpose. Not “build a reliable platform” — that is a property, not a direction. The real answer connects technical work to business outcomes and to user needs in a specific and falsifiable way.

Something like: we are building a data infrastructure that lets our analysts answer any product question within four hours without engineering support. Or: we are rebuilding our checkout system to reduce payment failure rates below one percent so we can expand to markets where card infrastructure is unreliable. Or: we are investing in observability tooling so that the time between a production anomaly and a diagnosis is under fifteen minutes.

These are statements that have a clear definition of success attached to them. They tell engineers not just what to build but why, and they connect technical decisions to outcomes that the business actually cares about. When your technical vision has that kind of specificity, it becomes a useful decision-making tool. Engineers can look at a proposed piece of work and ask: does this move us toward the goal? If the answer is no, it does not necessarily mean the work is wrong, but it raises a question that is worth answering.

Without that specificity, technical vision tends to drift into vague aspiration. “We want a clean, scalable, well-tested codebase” is aspirational, but it does not tell anyone what to prioritise when they have to choose between three competing things. Vision that cannot guide a tradeoff is not really vision. It is decoration.

Aligning technical direction with product and business goals is where most of the actual work of technical leadership happens, and it is messier than the frameworks make it sound. Product and engineering are not always pulling in the same direction. Business stakeholders have timeframes and objectives that do not always map cleanly onto technical realities. There are genuine tensions that have to be surfaced and worked through, not papered over.

The most effective technical leaders I have observed spend a disproportionate amount of their time in conversations that are not with their engineering team. They are in product reviews. They are in quarterly planning sessions. They are talking to the finance team about what the infrastructure cost structure actually looks like. They are in customer calls listening to the friction points that end up becoming product requirements six months from now. They are building enough context about the business that their technical direction is grounded in something real rather than purely in engineering instinct.

This matters because technical decisions made in isolation from business context tend to be technically elegant and practically wrong. A beautiful microservices architecture might be the right long-term call, but if the business needs to move fast in the next twelve months and your team has four engineers, it might be the wrong short-term one. The technical leader who understands the business well enough to make that call explicitly, rather than defaulting to the architecturally “correct” answer, is significantly more valuable than one who does not.

Roadmap ownership is the operational expression of technical vision. A technical roadmap is not a feature list. It is a sequenced set of investments that tell a coherent story about how the system is evolving and why. The sequencing matters as much as the content, because it reflects your actual prioritisation under real constraints rather than an idealised list of everything you want to do.

When I think about what makes a good technical roadmap, a few properties matter. It is honest about current state. It does not pretend that the existing system is further along than it is, because that dishonesty creates misleading expectations. It distinguishes between investments that enable future capability and investments that address current risk. Those are different categories with different urgency profiles. It has explicit tradeoffs documented. When you chose to sequence thing A before thing B, there was a reason. Write it down so that when circumstances change and someone asks “can we move B up?”, you have the context to answer that question thoughtfully rather than from gut instinct.

And crucially, the roadmap is a living document, not a contract. One of the most common problems I see with technical roadmaps is that they get published once and then decay. The codebase evolves, the product requirements shift, new information comes in — and the roadmap that was accurate six months ago is now quietly misleading anyone who reads it. Treat your roadmap as something that needs regular maintenance, not something that needs a polished quarterly reveal.

Communicating technical direction is a distinct skill from having it, and it is one that engineers who move into senior leadership roles often underestimate. The way you talk about technical strategy to your engineering team is different from how you talk about it to your product counterparts, which is different again from how you talk about it to executive stakeholders.

To your engineering team, the goal is to give people enough context that they can make good local decisions without needing to escalate everything. You want them to understand the direction well enough that when they encounter an ambiguous situation, they can reason about what the right call is rather than waiting for guidance. That means going deep on the why: why is this the direction, what are the tradeoffs we considered, what would make us revisit this. Engineers who understand the reasoning behind a decision are better positioned to execute against it and to flag when something they are seeing in the work suggests the reasoning might be wrong.

To product and business stakeholders, the translation layer is about outcomes and risk, not architecture. “We are building a service boundary between the payments system and the billing system” is not a compelling framing for a product conversation. “We are making a structural change that will let us run payment experiments independently of billing changes, which should cut our experiment cycle time by about half” is a compelling framing, because it connects the technical work to something product actually cares about.

Getting good at this translation is not about dumbing things down. It is about understanding what the other person actually needs to know to make good decisions or to trust that you are making good decisions. That is a different question than “what do I know about this topic.”

One of the tensions that comes up consistently in technical leadership is the relationship between strategic investment and near-term delivery pressure. There is almost always more pressure to ship features than there is to invest in the platform, pay down debt, or make structural improvements that will pay off over time. That pressure is legitimate — the business has real needs — but if it never gets balanced against platform investment, you end up in a state where the platform is actively fighting your ability to deliver.

The framing that I have found most useful for navigating this tension is to treat platform investment not as something that competes with delivery but as something that enables delivery over a longer time horizon. The argument is not “we need to stop shipping features to fix the platform.” The argument is “here is what the platform investment unlocks, here is the timeline on which it pays off, and here is the cost we are currently paying by deferring it.” That framing, with specific numbers attached when possible, tends to land better with business stakeholders than an abstract argument about technical health.

It also requires you to do the work of actually knowing what the cost of the current state is. That means having data. How much engineering time per sprint is going to work that would not be necessary if the platform were in better shape? What is the approximate blast radius of the current debt in terms of velocity impact? How many incidents in the last quarter were attributable to the areas you are proposing to invest in? This is the kind of evidence that turns a technical argument into a business argument.

There is a version of technical leadership that is mostly reactive: responding to product requests, triaging incidents, making tactical calls as they come up. That version can look like strong leadership from the outside because things are moving. But it does not create the conditions for the team to do its best work over time. The conditions for sustained, high-quality engineering output require a clear technical direction, explicit prioritisation, and the kind of structural investment that only happens when someone with enough context and enough authority is thinking more than one quarter ahead.

That is what technical vision is actually for. Not the vision document, not the architecture diagram, not the quarterly roadmap review. The underlying habit of mind that keeps asking: given everything we know about where this product and this business are going, are we building the system that will get us there? And if the honest answer is no, being willing to say so and to do something about it.

That willingness is what separates technical leaders from technical managers. And it is more about courage than it is about technical knowledge.

Next in the series: The Engineering Manager as Coach — practical techniques for performance conversations, growth planning, and career development.

Building Psychological Safety in Engineering

Let me ask you something uncomfortable: when was the last time someone on your team told you they had made a mistake before you found out about it yourself?

If you have to think hard about that, or if the honest answer is “I am not sure that has ever happened,” you probably have a psychological safety problem. Not a people problem. Not a talent problem. A culture problem, and one that sits squarely in your lap as the person responsible for setting the conditions under which your team operates.

Psychological safety is one of those concepts that gets talked about in engineering leadership circles to the point where it starts to feel like a buzzword. But the research behind it is solid, and the practical reality is straightforward: engineers on teams where they feel safe to take risks, speak up, and make mistakes without fear of punishment consistently outperform engineers on teams where they do not. Google’s Project Aristotle found it was the single biggest predictor of team effectiveness across hundreds of internal teams. It matters more than individual talent, more than technical skill, more than compensation.

So why is it so rare? Partly because it is genuinely hard to build. Partly because the behaviours that build it are counterintuitive for technically minded people who have been rewarded their whole careers for being right. And partly because leaders often think they have it when they do not, because the absence of psychological safety is largely invisible from the top.

Here is what I mean by that. When engineers do not feel safe, they do not tell you. They work around problems quietly. They do not raise concerns in meetings. They stay silent when they disagree. They fix bugs without mentioning them. They leave rather than confront a difficult situation. From your vantage point as a manager, everything might look fine. The problems are just invisible, which is exactly what makes the culture so hard to improve from the inside if you are not actively looking for the signals.

So let’s talk about what to look for, what to build, and what to stop doing.

The first signal to watch is who speaks in group settings. In a psychologically safe team, contributions are distributed. Junior engineers raise concerns. Senior engineers acknowledge uncertainty. Different people push back on ideas in different meetings. If you notice that the same two or three people do all the talking, or that nobody ever disagrees with whoever has the most tenure, or that critical questions always come up in private after the meeting rather than in the meeting itself, those are meaningful signals. The information is moving, but it is moving through channels that feel safer, which means you are not actually getting it when and where it matters.

The second signal is how the team handles incidents and failures. This is the clearest window into your culture. When something breaks in production, what happens? Is the post-mortem focused on understanding the failure and improving the system, or does it subtly or not so subtly focus on who was responsible? Do engineers run toward an incident or away from it? Do people escalate early when something is going wrong, or do they hold out hoping to fix it themselves rather than surface the problem?

I have seen teams where engineers would rather work through the night on a production issue than escalate because they were afraid of how the escalation would be received. That is not dedication. That is fear. And it is a direct result of an environment where being the person who raised the alarm felt more dangerous than trying to quietly fix the problem.

The blameless post-mortem is the standard recommendation for addressing this, and it is a good one – but only if it is actually blameless. A lot of post-mortems that are nominally blameless still subtly assign fault through the framing of their questions: “why did the engineer merge without a code review” rather than “what in our process allowed this change to go out without sufficient review.” The difference in those two questions is the difference between a culture that learns and a culture that punishes while pretending not to.

Write your post-mortem templates with this framing explicitly built in. Questions like: what in our system made this failure possible, what would have needed to be different for this to have been caught earlier, what can we change about our process to prevent this class of failure. Not: who approved this, why was this not caught, whose responsibility was this. If you run post-mortems this way consistently, over time it changes how the team thinks about failure – as information about the system, not as evidence of someone’s incompetence.

Feedback loops are the second major lever. A team without healthy feedback loops is a team where problems silently compound until they become crises. Building those loops requires two distinct things: creating channels for feedback to flow, and demonstrating through your own behaviour that feedback is welcome and acted on.

The first part is structural. Regular one-on-ones where you ask specific questions rather than open-ended check-ins. Retrospectives that have genuine psychological safety built into them (anonymous input tools can help here if the team is not yet comfortable with open discussion). Skip-level conversations if your org is large enough. Anonymous pulse surveys for tracking sentiment over time. These are all mechanisms for surfacing information that would otherwise stay invisible.

The second part is behavioural and it is more important. If engineers give you feedback and nothing happens, they stop giving feedback. If you react defensively when someone raises a concern, they stop raising concerns. If you say you want honesty but subtly reward people who tell you what you want to hear, you will get people who tell you what you want to hear.

The most powerful thing you can do to build feedback culture is to model receiving feedback well. When someone tells you something uncomfortable, thank them specifically for the specificity of the feedback. Then act on it and tell them you acted on it. Do this consistently and over time it signals that feedback in this team is not just tolerated but genuinely valued and used. That signal compounds. People talk to each other about how you respond, and your reputation as someone who receives feedback well becomes one of the structural features of your team’s culture.

Ask for feedback on yourself directly and explicitly. Not “any feedback for me?” in a one-on-one where the power dynamic makes honest negative feedback almost impossible. More specific prompts: “I ran that planning session last week – was there anything about the format that was not working for you?” or “I made a call on the architecture last month and I have been wondering whether I handled that well – what was your read on it?” Specific questions lower the barrier enough that more honest answers become possible.

The third lever is how you respond to mistakes, and this is the most visible signal your team gets about what the culture actually is regardless of what you say it is.

When an engineer makes a significant mistake – pushes a bug to production, misestimates badly, handles a difficult customer situation poorly – your response in that moment is teaching the whole team what the rules are. Not just the person involved. Everyone who hears about it is watching what happens.

The response that builds psychological safety has a few consistent properties. It is proportionate to the situation. It focuses on understanding what happened and learning from it rather than on assigning fault. It is private when the situation calls for privacy. It treats the person as an intelligent adult who does not need to be punished but who does need support in improving.

None of that means being soft on genuinely unacceptable behaviour. If someone repeatedly ignores code review conventions, ships without testing, or behaves disrespectfully to a colleague, those are different situations that call for direct and specific feedback. The distinction is between mistakes (things that happen despite good intentions and reasonable effort) and patterns of behaviour that reflect poor judgment or disrespect. Psychological safety is not about protecting people from the consequences of repeated poor behaviour. It is about ensuring that honest effort and reasonable risk-taking are not punished.

There is a specific failure mode worth naming here because it is common and because managers who fall into it often do not know they are doing it. It is what I think of as the chilling effect of public criticism. When a leader criticises an engineer’s work in front of the team (in a code review, in a meeting, in a Slack thread where multiple people are watching) the impact is not contained to that one person. Every engineer on the team who sees it learns something about the risk of being visible and vulnerable. The criticised engineer may recover. But the observation travels further than you think, and it quietly teaches people to keep their heads down.

Code review is where this plays out most often in engineering teams. Reviews where every comment is framed as a problem to be fixed, where reviewers never acknowledge what is working well, and where tone is dismissive or impatient — that culture compounds over time and eventually starts to affect how openly engineers communicate and how willing they are to take on work that is outside their comfort zone.

It is worth reviewing the norms around code review explicitly with your team. Not to make code review soft, but to make it genuinely useful. The goal of code review is to improve the code and grow the engineer. Both parts matter. A review that consistently improves the code but leaves the engineer feeling beaten up is a review that is failing at half its job.

One last thing that does not get enough attention: the connection between psychological safety and retention. Engineers leave teams for a lot of reasons, but one of the more consistent ones is the slow erosion of feeling like they can do their best work. That erosion is usually not a single incident but an accumulation of small signals – a feedback that was not heard, a mistake that was handled poorly, a concern that was dismissed, a pattern of who gets credit and who does not. It is invisible until someone puts in their notice, at which point the manager usually asks “what happened?” without realising the honest answer is “a lot of things, over a long time.”

Psychological safety is not a team-building exercise. It is not a workshop or a values statement. It is the cumulative result of thousands of small interactions

  • how you respond to a mistake
  • how you run a post-mortem
  • how you receive feedback
  • how you handle a tense code review

That either add up to a culture where people feel safe enough to do their best work, or they do not.

You build it slowly and you can damage it quickly. That asymmetry is worth keeping in mind every time you have one of those small interactions. Because you are always, whether you realise it or not, signalling to your team what the rules are.

Next in the series: Strategy vs. Execution — how senior engineering leaders align product, technology, and business goals into a coherent technical direction.

Where Is My Website Calling Home? — Forge CMS 2026.3.27

A git-backed, cache-resilient, privacy-first CMS – and a live proof of concept born in Wittenberg.

“Where Is My Website Calling Home?”

If you’ve ever installed WordPress and added a plugin – any plugin – you’ve probably asked yourself this question at some point.

Google Analytics. Facebook Pixel. Some random jQuery CDN. A newsletter widget that reports back to a server in Virginia.

Your website is having conversations you never started.

With Forge CMS 2026.3.27, we’re doing things differently.

What Is Forge CMS?

Forge CMS is a lightweight content management system built on the Forge 4D stack. It uses SML (a declarative UI language inspired by QML) to define pages and layouts, runs on a VPS, and serves static-style content with zero JavaScript framework overhead.

No React. No Next.js. No npm install rabbit hole.

Just clean, readable markup – and now, with 2026.3.27, a proper content pipeline.

What’s New in 2026.3.27

1. Git-Backed Content – Version History for Free

All page content is now loaded from a Git repository that you specify in the server config.

content_repo: https://codeberg.org/YourOrg/your-site

That’s it. Your entire website lives in a repo. You get:

  • Full version history – every edit is a commit
  • Rollback – one git revert and you’re back
  • Collaboration – PRs, reviews, branches – standard Git workflow
  • Flexibility – Codeberg, GitHub, Gitea, your own Forgejo instance – whatever you trust

And if you want to switch repos at runtime? You can. Swap the config, restart – the new repo is live. This means you can set up a fallback repo (your own server, GitHub mirror, whatever) and pivot within minutes if your primary goes down.

During the writing of this article, Codeberg went fully offline – “Service unavailable.”

The live site at atesti.crowdware.info kept serving without interruption – from VPS cache.

Same day as the release tag. Unplanned. Perfect.

2. Resilient Caching – The Site Stays Up

Even if the source repo becomes unreachable, Forge CMS keeps serving from its VPS-side cache.

This isn’t a CDN. There’s no Cloudflare account, no monthly bill, no Terms of Service to read. It’s just the server remembering what it already fetched – and staying calm when upstream has a bad day.

Practically: your site survives your Git host’s outage.

We tested this accidentally today. It worked.

3. Umami Analytics – Opt-In, Commented Out by Default

WordPress ships with Google Analytics baked into half its popular themes. You install a theme, you get tracking. Surprise.

In Forge CMS, analytics is a single SML element:

// Umami analytics - uncomment to enable
// Umami {
//     websiteId: "your-id-here"
//     src: "https://your-umami-instance/script.js"
// }

Commented out. By default. On purpose.

When you do enable it, it points to Umami – an open source, self-hostable, GDPR-friendly analytics tool. No cookies. No cross-site tracking. No phone calls to Mountain View.

You know exactly what it tracks, where the data goes, and how to turn it off.

That’s the deal.

Live Proof: Atesti para Dana

The first real-world deployment of Forge CMS 2026.3.27 is atesti.crowdware.info – a site for Atesti para Dana, a local currency experiment born on March 25, 2026 in Wittenberg, Germany.

The concept: every existing currency begins with debt or promise. Atesti begins with a completed act of giving – witnessed by three people. Only then is a “GUT-Schein” (goodness-certificate) issued. No debt. No promise. Just something that already happened.

The site is intentionally local-first. It’s in German. It’s for Wittenberg. The seed doesn’t need to travel fast – it needs to land well.

But the code that serves it? That’s for everyone.

The Stack, Briefly

Layer Technology
CMS engine Forge CMS (Go)
Page language SML (declarative, QML-inspired)
Content source Git repo (Codeberg / GitHub / self-hosted)
Analytics Umami (self-hosted, opt-in)
Hosting VPS (no app store, no cloud lock-in)
License GPL3 + commercial (dual)

Why This Matters

The web is full of CMS platforms that are technically free but philosophically expensive. They trade your users’ attention and data for convenience.

Forge CMS makes a different trade: a little more intentionality upfront, in exchange for knowing exactly what your site does.

Git for content. Cache for resilience. Umami for honest analytics.

And when your Git host has a bad day – your site just shrugs.

Get Involved

Aho. 🌱

Our atesti site may be loading a bit slow, because it checks against the repo if there are newer versions. And when the host is slow, this will also make the website slow, even when content is cached.

Also I studied Web- and GraphicsDesign, but I am too lazy to write CSS. Just wanted to show a very basic website 😉

Art – Wittenberg, March 27, 2026

License: GPL3 + commercial dual license

Scaling Engineering Teams Without Losing Velocity

Here is something that surprises almost every engineering manager the first time they live through it: growth slows you down before it speeds you up.

You hire five more engineers. You expect output to increase proportionally. Instead, things get messier. PRs take longer to review. Standups run over. Someone built a feature that conflicts with something another team was working on. The planning process that worked for eight people completely breaks down at fifteen. Velocity, by most measures, actually drops.

This is not a failure of hiring. It is a failure of org design. And it happens so reliably, at so many companies, that it has become one of the most predictable traps in engineering leadership.

The good news is that it is largely avoidable if you think about the structural side of growth before you need it, rather than after things are already falling apart. That is what this article is about. Not headcount strategy in the abstract, but the practical mechanics of how teams grow: org structure, hiring cycles, cross-team coordination, and the specific bottlenecks that kill velocity as you scale.

I want to start with the thing most scaling guides skip, which is the relationship between team size and communication overhead. It is not intuitive until you see the numbers.

When you have a team of four engineers, there are six possible communication paths between them. Add a fifth person and you get ten. Add a sixth and you get fifteen. The formula is n(n-1)/2, and the point is not to memorise it but to understand the curve. Every person you add increases the number of relationships, dependencies, and potential misalignments on the team. At some point, that overhead exceeds the capacity benefit of adding another person.

This is what Brooks’ Law is actually pointing at when it says adding manpower to a late software project makes it later. It is not that people are useless. It is that onboarding and coordination costs are real, and if you add people without managing the structural overhead, you pay more than you gain.

The practical implication is that teams above a certain size need organisational structure to remain effective, not because structure is inherently good, but because it reduces the surface area of coordination that each person has to manage. Smaller, autonomous teams with clear ownership can each operate with low overhead while the aggregate output scales. This is the core logic behind the two-pizza team concept, and it holds up in practice even if the pizza metric itself is a bit silly.

So when should you split a team? The signals I watch for:

  • when more than half your standup is irrelevant to most of the people in it
  • when engineers are regularly blocked waiting for decisions from a single point of authority
  • when the codebase has grown to the point where nobody fully understands the system end-to-end
  • when you have grown past eight or nine people and the coordination overhead is visibly slowing things down.

Any one of those is a reasonable trigger. Two or more at once and you are already overdue.

How you split matters as much as when. The worst splits are by technical layer; frontend team, backend team, infrastructure team. That structure creates handoff-heavy workflows where a single feature requires coordination across three teams and nobody owns the outcome end-to-end. Conway’s Law tells you that your org structure will reflect your system architecture, so if you want loosely coupled systems, you need loosely coupled teams with bounded ownership. Organise around product domains or user-facing capabilities instead. A team that owns the payments experience end-to-end – from the frontend interaction to the backend logic to the database schema – can move fast without waiting on anyone else.

The flip side of this is that you need to be deliberate about shared infrastructure. When teams own their domains end-to-end, they will inevitably have shared concerns: authentication, logging, deployment pipelines, internal libraries. Left unaddressed, every team solves these independently and you end up with seven slightly different implementations of the same thing. The solution is a platform or infrastructure function – a small team explicitly chartered to own the things that are shared – with a strong bias toward building self-service tooling rather than becoming a bottleneck that product teams have to request work from.

Now let’s talk about hiring cycles, because this is where a lot of scaling plans go wrong in a different way.

Hiring has a lag. From the moment you open a role to the moment a new hire is meaningfully contributing is typically four to six months, sometimes more. That includes recruiting time, interview process, notice period, onboarding, and ramp-up. If you wait until your team is visibly overloaded to start hiring, you will be waiting for relief for a long time while the people already on your team burn out.

The right time to start a hiring cycle is before you need the capacity, not when you need it. That requires a planning horizon most engineering managers are not used to operating on. You need to look three to six months out and ask: what does this team need to look like at that point, given what we have committed to delivering? Work backward from that and you will usually find that you needed to start hiring two months ago.

This requires building a relationship with your recruiting function and being a genuine partner rather than just a consumer. Understand what is in the pipeline. Give fast, specific feedback on candidates. Keep your job descriptions current. Show up to sourcing conversations. Engineering managers who treat recruiting as someone else’s job tend to have longer time-to-hire and weaker candidate pools than those who engage actively.

One thing worth doing if you have the data: track your actual time-to-productivity for new hires by role and seniority. Not time-to-hire, but time-to-first-meaningful-contribution and time-to-full-productivity. That data tells you how far ahead you need to plan, and it often reveals onboarding gaps that are adding weeks of unnecessary ramp-up time.

Cross-team handoffs are where velocity goes to die once you have multiple teams. I have seen this pattern so many times it is almost a cliche: two teams are working on adjacent parts of a system, the interface between them is not well defined, and what should be a clean integration turns into a weeks-long negotiation over API contracts, data ownership, and who is responsible for what.

The solution is not more meetings between teams. It is clearer ownership and better written contracts upfront. Before work begins on anything that spans team boundaries, you want a written definition of: who owns what, what the interface looks like, what the expected behaviour is, and what the escalation path is when something goes wrong. This does not have to be elaborate. A short technical spec or an RFC that both teams have reviewed and agreed to is usually enough.

Some teams formalise this with an internal RFC process, where any significant technical change that affects other teams requires a written proposal with a comment period before work begins. Done lightly, this is genuinely useful. Done heavily, it becomes bureaucratic overhead that slows everything down. The right level depends on your team size and the pace of change in your system. At ten engineers, a quick shared doc and a Slack thread is probably enough. At fifty, you probably want something more structured.

Dependency management is a related problem that gets worse as teams scale. When team A is blocked on team B’s work, that is a velocity killer. The standard advice is to minimise cross-team dependencies, which is correct but not always achievable. When dependencies are unavoidable, the goal is to surface them as early as possible, sequence work so that the dependent team can start on other things, and have a clear escalation path when a dependency is at risk of slipping.

This is one of the things that makes engineering planning genuinely hard at scale. You are not just sequencing work within a team. You are managing a dependency graph across multiple teams, each with their own priorities and constraints. The teams that do this well treat inter-team dependencies as first-class items in planning – visible, tracked, and actively managed – rather than background assumptions that only surface when they blow up.

Let me talk about a specific bottleneck that I see consistently kill velocity in growing teams: the single point of decision authority.

It usually starts innocuously. A senior engineer or tech lead has strong opinions and good judgment, so decisions naturally flow through them. This works fine at small scale. At scale, it creates a queue. Every architectural decision, every significant PR, every cross-cutting concern sits waiting for one person’s input. That person becomes increasingly overloaded and increasingly a blocker, not because they are doing anything wrong but because the structure has not kept up with the team’s growth.

The solution is to distribute decision-making authority deliberately. That means being explicit about who has authority over what class of decision, investing in documentation and principles that allow people to make good decisions independently, and creating a culture where “I made a call and here is my reasoning” is the norm rather than “I waited for someone to tell me what to do.”

This requires accepting that some decisions made without your input will be decisions you would have made differently. That is an uncomfortable tradeoff. But a slightly suboptimal decision made quickly and autonomously is almost always better for team velocity than the perfect decision that took two weeks to reach because everything had to go through one person.

The other bottleneck worth naming explicitly is the release process. Teams that can deploy independently and safely move faster than teams that share a deployment pipeline or a release schedule. If your release process requires coordination across multiple teams or a human approval gate that creates a queue, that is a structural constraint on your velocity that hiring more people will not fix. Investing in a deployment pipeline that gives teams independent, safe, fast release capability is one of the highest-leverage infrastructure investments you can make as you scale.

There is a meta-principle underlying most of what I have described: the goal of organisational design as you scale is to keep the blast radius of any individual team’s decisions small while maximising their autonomy within that boundary. Small blast radius means that when a team makes a mistake, it does not cascade across the whole system. High autonomy means the team can move fast without waiting for permission or coordination.

Those two things are in tension, and the job of engineering leadership is to find the right balance for your organisation at its current size and stage. The balance shifts as you grow. What worked at fifteen engineers will break at forty. What works at forty will need to change at a hundred. The managers who navigate this well are the ones who keep asking “is our structure still serving us, or are we serving our structure?” and are willing to change the answer when it needs to change.

Growth is not just an execution problem. It is a design problem. The teams that scale well treat their org structure, hiring cadence, and cross-team coordination as things that need to be actively designed and maintained, not defaults that exist until they break.

Start designing before you need to. You will be glad you did.

Next in the series: Building Psychological Safety in Engineering — creating a culture where failure is a learning opportunity, not a liability.

Automate SEO Linking Without Losing Control in WordPress!

I built a WordPress plugin that solves one of the most tedious yet critical SEO tasks: internal linking. The problem? Manually connecting hundreds (or thousands) of posts is time-consuming, error-prone, and nearly impossible to scale. But blind automation risks irrelevant links or over-optimized anchor text. My solution bridges this gap with AI-powered linking that respects editorial control.

Under the hood, the plugin uses semantic embeddings to understand content relationships, not just keyword matching. When you publish a post, it analyzes the entire site’s indexed content, scores potential link targets based on relevance and authority, and suggests the best connections. The twist? You retain full control. Every suggestion appears in a dedicated editor panel where you can tweak anchor text, reject links, or bulk-apply with a threshold for confidence scores.

For large sites, a bulk processor handles site-wide linking in the background. It identifies orphan pages (content with no incoming links) and rescues them by connecting them to high-authority sources. A visual graph maps your entire link structure, letting you spot weak connections or overlinked content instantly. The plugin even flags repetitive anchor text to avoid SEO penalties.

The real magic is the hybrid approach: AI does the heavy lifting of analyzing content and generating suggestions, while you decide what gets applied. It’s automation without sacrificing precision. Check it out here if you’re tired of manual linking but wary of losing control.

Hiring Engineers: A Manager’s Playbook

Hiring is one of the highest-leverage things you will do as an engineering manager. A single great hire compounds over years. A single bad hire – or more precisely, a bad hiring process that lets the wrong person through while filtering out the right ones; costs you more than you think. Not just in time and salary, but in team morale, velocity, and the invisible tax of managing a poor fit.

Most engineering hiring processes are broken in ways that managers do not fully recognise until they have been on the other side of them. I have sat in hiring debriefs where five engineers gave five different verdicts on the same candidate and nobody could articulate why they felt the way they did. I have seen candidates with brilliant portfolios fail whiteboard tests that had nothing to do with the actual job. I have watched great engineers get filtered out because they had a quiet interview style and the interviewer mistook silence for incompetence.

The goal of this article is to help you build a process that is structured enough to reduce those failure modes, flexible enough to surface genuine talent, and honest enough to actually tell candidates what working on your team is like. That last one matters more than most people realise. Hiring is a two-way evaluation, and the best candidates have options.

Let’s walk through the full arc: structure, technical assessment, culture and values alignment, and onboarding. Not as separate checklists but as a coherent system that you design intentionally.

Defining what you are actually hiring for is the step most hiring processes skip or do half-heartedly. A job description is not a definition of success. “Strong communication skills and a passion for technology” is not a definition of success. Before you post a role, sit down with a blank document and answer three questions: what does this person need to be able to do in the first ninety days to be considered a strong hire? What do they need to be able to do after a year? What specific gaps on the team are we trying to close?

Those answers should drive everything else. The technical assessment should test for the actual skills the role requires. The interview questions should probe for the actual behaviours the role demands. The evaluation criteria should map directly to those definitions of success. When you have that clarity upfront, the rest of the process becomes much easier to design and much harder to game.

It also forces an honest conversation about scope. Are you hiring a senior engineer to independently own complex technical problems, or a mid-level engineer to execute on well-defined work? Those are different roles, different assessments, and different interview conversations. Conflating them (which is extremely common) leads to either overhiring for the work or underhiring for the scope and setting someone up to fail.

The interview structure itself should follow a consistent format for every candidate for the same role. Not because consistency is a bureaucratic virtue but because without it, you cannot make fair comparisons. If candidate A had a rigorous technical discussion and candidate B got a casual conversation about their career history, your debrief is comparing two different things. Structured interviews reduce that problem significantly.

A reasonable interview structure for a senior engineering role looks something like this. A recruiter or hiring manager screen that covers career background, motivations, and basic role fit; thirty minutes tops, mostly conversational. A technical assessment that gives you signal on the actual skills the role requires; more on format in a moment. A values and working-style conversation with one or two people on the team. And a final conversation with the hiring manager that covers the role more deeply, answers the candidate’s questions, and gives you a chance to assess how they think about their work at a higher level.

The exact structure will vary by seniority and team. But the principle is the same:

  • each stage should have a clear purpose
  • a clear set of things it is trying to evaluate
  • a consistent format so that different candidates are being measured against the same bar.

Technical assessment is where the most disagreement happens and where the most damage is done. Let me be direct about what I think: timed algorithm puzzles and whiteboard problems that bear no resemblance to the actual job are a poor way to assess most engineering roles. They test a specific kind of performance under artificial pressure that correlates poorly with day-to-day engineering work. They also systematically disadvantage experienced engineers who have been in industry roles and simply do not practice leetcode.

That does not mean technical assessment is not important. It absolutely is. It means the assessment should reflect the actual work.

For most backend engineering roles, a practical take-home that asks the candidate to do something roughly analogous to what they would do on the job is a better signal than a timed algorithm test. Something like: here is a small API, extend it with this feature, write tests, and leave notes on any tradeoffs you made. That tells you how someone actually writes code, how they think about testing, how they communicate their decisions; all things that matter in the role. You can review it asynchronously, which is fairer to candidates in different timezones or with jobs that make synchronous sessions difficult.

If you do a live technical session, make it collaborative rather than evaluative in the traditional sense. Work through a problem together. Let the candidate look things up. Ask them to walk you through their reasoning. That is a much closer simulation of actual engineering work than asking someone to produce a correct answer under observation with no resources.

One thing worth doing regardless of format: review your technical assessment regularly to check whether it is actually predicting performance. If you can look back at candidates you hired and compare their assessment performance to their on-the-job performance, you will often find that the correlation is weaker than you assumed. That is useful information for calibrating the assessment over time.

Culture fit is a phrase that gets misused constantly, so let’s reframe it. What you are actually evaluating in a “culture fit” conversation is values alignment and working style compatibility. Not whether someone shares your hobbies or went to the same kind of school. The distinction matters because culture fit as commonly practiced is one of the most reliable vectors for homogeneity in hiring.

Values alignment is about the things that actually affect how someone works: how they handle disagreement, how they respond to ambiguity, how they communicate when something is going wrong, whether they default to transparency or opacity under pressure, how they think about ownership. Those are things you can probe for directly with behavioural questions.

Behavioural questions should be specific and past-oriented. “Tell me about a time when you had to push back on a technical decision you disagreed with” gives you real signal. “Are you someone who speaks up when you disagree” gives you the answer the candidate thinks you want. The difference in data quality between those two questions is significant.

A few questions that reliably surface useful signal across seniority levels:

  • Tell me about a time a project you were responsible for went significantly off track. What happened and what did you do?
  • Tell me about a piece of feedback that genuinely changed how you work.
  • How do you typically approach a technical decision when the right answer is not obvious?
  • Tell me about a time you worked with someone whose working style was very different from yours.

What you are listening for is not the story but the self-awareness. Does this person understand their own patterns? Do they take ownership of things without being defensive? Do they demonstrate genuine curiosity and willingness to learn? Those qualities matter across almost every technical role and almost every team culture.

The debrief structure matters as much as the interview itself. A common failure mode is the unstructured debrief where the first person to speak sets the frame and everyone else anchors to their opinion. This is how groupthink gets baked into hiring decisions.

A better approach: everyone writes their evaluation independently before the debrief begins. Not a detailed report, just a hire or no-hire recommendation and a few bullet points on the key evidence for each dimension you were evaluating. Then in the debrief, go around the room before any open discussion. Once everyone has shared their read, then you discuss disagreements.

This surfaces more honest signal and makes disagreements more productive. When two interviewers have very different reads on the same candidate, that disagreement is itself information worth understanding. Sometimes it means the candidate gave different answers to different people. Sometimes it means the interviewers were evaluating for different things. Either way, working through it leads to a better decision.

Define your evaluation rubric before the process starts, not after. Dimensions like technical depth, communication clarity, problem-solving approach, and ownership mindset should have concrete descriptions of what strong, acceptable, and weak looks like for the role in question. Rubrics are not bureaucratic overhead. They are the thing that makes your process defensible and improvable.

Onboarding is the part of the hiring process that most engineering managers treat as someone else’s responsibility. It is not. The way you integrate a new engineer into the team in their first sixty days has a direct effect on how quickly they become productive, how connected they feel to the team, and whether they stay past the twelve-month mark.

A good engineering onboarding plan has a few consistent elements. It starts before day one: send the new hire an onboarding doc before they start so they know what to expect in week one. Not a firehose of information, just an overview of what the first few weeks look like and who they will be meeting.

In the first week, the goal is orientation not productivity. Get their environment set up, walk them through the architecture at a high level, introduce them to the key people they will work with, and give them something small but real to ship. That first commit or merged PR is important for psychological reasons; it makes the new hire feel like a contributor rather than an observer.

In weeks two through four, graduate the complexity. Give them increasingly meaningful work with explicit context about why it matters and what good looks like. Run a proper one-on-one at the end of week one, week two, and week four specifically to check in on how the onboarding is going and surface any confusion or friction early.

Assign a buddy. Not a formal mentor relationship, just someone on the team who has been around for a while and is available to answer the questions the new hire is too self-conscious to ask their manager. Questions like “where does this documentation actually live” or “is it normal that this service takes fifteen minutes to build” are exactly the kind of thing a buddy handles well and that a new hire will not raise in a one-on-one for fear of seeming underprepared.

At the sixty-day mark, have an explicit conversation about how the onboarding has gone. What worked? What was confusing? What would have helped to know earlier? This is partly for the new hire’s benefit and partly for yours. The feedback you get from new engineers about your onboarding process is some of the most valuable signal you have for improving it, because they just experienced it with fresh eyes.

One broader principle worth naming: the hiring process is a product. It has users (candidates and interviewers), it produces an output (a hiring decision), and it can be improved iteratively based on data. Treat it that way.

  • Track your offer acceptance rate.
  • Track time-to-hire.
  • Talk to candidates who declined your offers and find out why.
  • Talk to new hires at the sixty-day mark and find out where the process was unclear or misleading.
  • Run a retrospective on your process once a quarter.

Most engineering teams never do any of this. They run the same hiring process for years without measuring whether it works, then wonder why their candidate pool is shallow or their offer acceptance rate is low. A small amount of deliberate iteration goes a long way.

The last thing I want to say about hiring is about honesty. Be honest with candidates about the role, the team, and the company. Tell them what is genuinely hard about working there. If your codebase has serious technical debt, say so. If the team is going through a restructuring, say so. If the role requires a lot of on-call, say so clearly and early.

This feels counterintuitive because you want to sell the role. But candidates who join with an accurate picture of what they are getting into are better retained and faster to trust you than candidates who discover the gap between the pitch and the reality in their first month. The best hiring conversations I have seen treat the candidate like an intelligent adult who deserves real information, not a polished brand narrative.

That respect carries forward into the relationship if they join. And it saves everyone a painful offboarding conversation six months later.

Next in the series: Scaling Engineering Teams Without Losing Velocity – org design, hiring cycles, and avoiding the bottlenecks that slow teams down as they grow.

I Benchmarked fasthttp vs net/http — The Results Surprised Me

I kept hearing that fasthttp was faster than Go’s standard net/http. So I decided to stop guessing and just measure it.

The result? 5.6x faster. Zero heap allocations. And the benchmark is only two files.

Keeping It Fair

The trick to an honest benchmark: remove everything except what you’re testing.

Both servers use the same in-memory listener — no TCP, no network noise. Same endpoints, same response bodies. The only difference is the HTTP stack.

// net/http
func SimpleHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
}

// fasthttp
func SimpleFastHandler(ctx *fasthttp.RequestCtx) {
    ctx.Write([]byte("Hello, World!"))
}

Same job. Very different performance.

The Numbers

Apple M2 Pro, Go 1.23.4

Benchmark net/http fasthttp Speedup
Simple 14,449 ns/op, 63 allocs 2,563 ns/op, 0 allocs 5.6x
JSON 15,561 ns/op, 72 allocs 3,136 ns/op, 6 allocs 5.0x
Parallel 3,541 ns/op, 63 allocs 1,437 ns/op, 0 allocs 2.5x

Zero allocations on simple requests. That’s not a typo.

The parallel gap is smaller (2.5x) because net/http already handles concurrency well. But even there — 63 allocations vs. 0.

What Makes fasthttp So Fast?

It comes down to one idea: don’t allocate, reuse.

net/http creates fresh Request and ResponseWriter objects for every single request. fasthttp pools them with sync.Pool and resets them between requests. That’s why you see 63 allocs drop to 0.

It also skips things most apps don’t need — header map cloning, chunked encoding by default, HTTP/2. Less work per request = less time per request.

So Should You Switch?

Use fasthttp when:

  • You’re building a proxy, gateway, or high-throughput service
  • Allocation pressure and GC pauses are a real problem
  • You need every microsecond on hot paths

Stick with net/http when:

  • You need HTTP/2
  • You rely on the Go middleware ecosystem
  • context.Context propagation matters to you

For most apps, net/http is the right choice. But when performance is the priority, fasthttp earns its name.

Run It Yourself

git clone https://github.com/soyvural/fasthttp-vs-nethttp.git
cd fasthttp-vs-nethttp
go test -bench=. -benchmem -count=3

Two files. No dependencies beyond fasthttp. Clone it, run it, see your own numbers.

Full source: soyvural/fasthttp-vs-nethttp

Got different results on your machine? I’d love to see them in the comments.