An Engineer's Code Tells You A Lot About The Engineer

An Engineer's Code Tells You A Lot About The Engineer

I was sitting at the back of a the MancJS meetup in Manchester last month, listening to Liam Walsh share a personal challenge: to prove—mostly to himself—that he was worthy of the title "senior developer". A great idea in theory. But also, let’s be honest, a fast track to imposter syndrome when you get tripped up on something that should be easy. Been there 😅.

Later in the evening, we broke off into groups to discuss how AI is affecting the industry. But by that point, my mind had already started to wander. Not out of boredom—just... drift. The kind of creative drift that shows up when you're supposed to be focused.

That’s when a word popped into my head:
Procrastidieation.

Procrastidieation (n.): a Cronenbergian fusion of procrastination and ideation; the act of generating unusually good ideas while avoiding what you're actually meant to be doing.
(Pronunciation unclear. Possibly unstable. May evolve on contact.)

And during this particular episode of procrastidieation™, my brain took a sharp turn toward an old childhood memory: a radio show where a handwriting expert would read people’s signatures like tarot cards. The curve of a "J" or the slant of a "T" was enough to reveal someone’s personality, insecurities, confidence—everything. For those curious about the name of this profession, it is Graphology 🤯.

That idea of personality encoded in form must’ve stuck, because here I am years later, writing software and still thinking about it. My sister, who happens to be studying both software engineering and psychology, might say it makes sense—code is just another kind of self-expression, after all. Even though we don’t talk much about her studies, that overlap has been quietly sitting in the back of my mind for a while now.

So this thought took shape:
What if we all write code in ways that reflect who we are?

Because let’s be real—when you read someone else’s code, you're not just looking for bugs. You're decoding their decision-making. You're noticing patterns. You're probably forming opinions. Their personality leaks through in the naming, the structure, the way they handle edge cases—or ignore them entirely.

That’s when the idea of developer archetypes took shape. Not rigid types, not personality tests—but loose, recognizable patterns. Personas that emerge in codebases everywhere. I've seen them in the wild, in PRs, in my own commits... 🙈

This isn’t about stereotyping—it’s about observing. Noticing. Reflecting. Maybe even laughing a little. Because whether you're the Justifier, the Hacker, or (heaven help your team) the Perfectionist... there’s something human in the way we all write code.

So, let’s take a walk through the archetypes. See who feels familiar. Maybe you'll even find yourself in there 😂.

🧙‍♂️ The Prophet

"The future is coming. I'm just building for it early."

You can usually spot The Prophet in the wild by the smell of abstraction. Their codebase is thick with modularity, optional flags, future-proofing, and ominous TODOs referencing requirements that don’t exist yet. If there's an interface for a single class—or a factory for just one kind of object—you might be in the presence of a Prophet.

They’re not necessarily wrong. In fact, they’re often brilliant. Their mental model includes business scenarios three-quarters ahead of the rest of the team. They’ve already anticipated edge cases no one’s considered. But here’s the catch: they’re usually bad at guessing the future.

"We are bad guessers."
— Sandi Metz, RailsConf 2016 - Get a Whiff of This

When taken too far, this mindset leaves behind a familiar smell in the codebase: Speculative Generality. It’s the trail of structures built not for current needs, but for imagined ones. Classes that could be extended. Configs that could be toggled. Hooks that could be injected. All designed to handle complexity that hasn’t arrived—and might never arrive.

The Prophet thrives in blank-slate projects. Give them a greenfield repo, and they’ll build a glorious cathedral of interfaces, hooks, listeners, and generic handlers—for users that haven’t signed up yet. But in a real-world codebase, their speculative architecture can become brittle, confusing, and slow to evolve once reality diverges from their vision. And it always does.

They often struggle with team buy-in, too. While they’re future-proofing, the rest of the team is just trying to get this week’s feature out the door. Documentation lags behind. Patterns feel over-engineered. And when something breaks? Good luck tracing it through the prophecy layers.

Still, you have to admire the ambition. 😂

💻 In the Wild: A Glimpse of the Future (That Never Arrived)

class PaymentHandler {
  constructor(paymentGateway) {
    this.paymentGateway = paymentGateway;
  }

  processPayment(type, amount) {
    if (type === 'credit_card') {
      return this.paymentGateway.charge(amount);
    } else if (type === 'paypal') {
      // Not implemented yet
    } else if (type === 'crypto') {
      // Reserved for future use
    } else {
      throw new Error('Unsupported payment type');
    }
  }
}

Simple Example

At first glance, this class seems harmless—it’s designed to process payments through multiple methods. But a closer look reveals the classic signs of Speculative Generality. Only one branch of logic (credit_card) is actually implemented, while the others are placeholders for functionality that doesn’t yet exist and may never be needed.

This isn’t inherently wrong—it shows some foresight. But it introduces unnecessary branching and complexity before there's any proven requirement for it. Future developers might assume this functionality is supported or get tripped up maintaining logic paths that were never meant to be live. It's a well-meaning prophecy that ends up cluttering the present.

interface StorageStrategy {
  save(data: string): void;
  load(): string;
}

class LocalStorageStrategy implements StorageStrategy {
  save(data: string) {
    localStorage.setItem('data', data);
  }

  load() {
    return localStorage.getItem('data') || '';
  }
}

class StorageManager {
  private strategy: StorageStrategy;

  constructor(strategy?: StorageStrategy) {
    this.strategy = strategy || new LocalStorageStrategy();
  }

  save(data: string) {
    this.strategy.save(data);
  }

  load(): string {
    return this.strategy.load();
  }
}

Complex Example

Here we have a cleanly architected example of The Prophet in action. The developer has created a full Strategy pattern for data storage, abstracting away the implementation details through an interface. On paper, this is solid design—but in practice, it’s speculative.

The only strategy implemented is LocalStorageStrategy, and there are no signs that alternatives (like IndexedDB, cloud storage, or file-based storage) are in the pipeline. So why introduce the abstraction now? While the intent is to make future change easier, this kind of design can create confusion: new developers will spend time trying to understand why the abstraction exists, or worse, assume there’s a reason they shouldn’t just use localStorage directly. Over-engineering in disguise.

This is a common Prophet pattern: reaching for architecture patterns like factories, strategies, or adapters before there's a real demand. It makes sense in theory, but in practice? It creates a mismatch between design and reality.

🧠 Reflections

If you see yourself in this archetype, you're probably someone who thinks deeply about architecture and change. You like to anticipate rather than react, and that foresight can be incredibly valuable. But in chasing the hypothetical future, you might unknowingly slow down the present. Discovering this archetype in your own habits is a chance to reflect on the balance between flexibility and overengineering. Spotting Speculative Generality in your work doesn’t mean you're wrong—it just means you may be building for a version of tomorrow that never quite arrives. The real power lies in knowing when to abstract, and when to wait.

🧾 The Auditor

"If we didn't document it, did it even happen?"

The Auditor’s presence is felt before you even look at the code. Their fingerprints are in the commit history, the README, the changelog, the RFC document linked in the ticket. Every decision—no matter how small—is logged, annotated, and explained. And then, of course, there’s the code itself, where every function header might as well come with a legal disclaimer.

They are the guardians of correctness. The watchers on the wall. The ones who ask, "But what happens if this input is null and the session has expired and the request comes from an unverified source IP?" And you know what? Sometimes, they save you from the abyss.

Security-conscious and process-driven, the Auditor tends to write code that is both defensive and exhaustive. They're the type to validate inputs three layers deep and include a ten-line comment about why a try/catch was necessary—even if it should be obvious. Their world is one of risk analysis, audit trails, and accountability. If the Prophet builds for the future, the Auditor builds to be understood in the future.

It’s no surprise that many Auditors either come from—or evolve into—compliance, security, or DevSecOps roles. They thrive in environments where trust has to be earned by every line of code. But in fast-moving teams, they can sometimes feel like a brake pedal. Their insistence on logging, checks, and explanations may slow down delivery, especially when paired with less structured teammates.

Still, when things go sideways? You’ll be glad the Auditor was here.

💻 In the Wild: A Paper Trail in Code

function updateUserProfile(userId, profileData) {
  if (!userId) {
    console.error('updateUserProfile: Missing userId');
    return;
  }

  if (!profileData.name || !profileData.email) {
    console.warn('updateUserProfile: Incomplete profile data', profileData);
    return;
  }

  console.info(`Updating profile for user ${userId}`, profileData);

  try {
    database.update(userId, profileData);
    console.log(`Profile update successful for user ${userId}`);
  } catch (err) {
    console.error(`Failed to update profile for user ${userId}`, err);
  }
}

Simple Example

This function doesn't just do its job—it documents every step of the way. From validating inputs to wrapping the database call in a verbose try/catch, it ensures that anyone reading logs later will know exactly what happened. Even the warnings are specific and traceable.

It’s not the most elegant or concise approach, but it’s classic Auditor territory: over-communication in the name of accountability. This code wasn't written just for the machine—it was written for the developer who might be debugging a 3am production failure six months from now. Whether that’s helpful or overbearing depends on the context—but it certainly leaves a trail.

interface AuditEntry {
  timestamp: string;
  userId: string;
  action: string;
  payload: any;
}

class AuditLogger {
  private entries: AuditEntry[] = [];

  log(entry: AuditEntry) {
    // In real life, this would likely be written to an external system
    this.entries.push(entry);
    console.debug('Audit Log Entry:', entry);
  }

  getLogsForUser(userId: string): AuditEntry[] {
    return this.entries.filter(e => e.userId === userId);
  }
}

function deleteUserAccount(userId: string, reason: string, logger: AuditLogger) {
  if (!userId || !reason) {
    throw new Error('User ID and reason are required for account deletion');
  }

  logger.log({
    timestamp: new Date().toISOString(),
    userId,
    action: 'DELETE_USER',
    payload: { reason }
  });

  try {
    userService.delete(userId);
    logger.log({
      timestamp: new Date().toISOString(),
      userId,
      action: 'DELETE_USER_SUCCESS',
      payload: {}
    });
  } catch (error) {
    logger.log({
      timestamp: new Date().toISOString(),
      userId,
      action: 'DELETE_USER_FAILURE',
      payload: { error: error.message }
    });
    throw error;
  }
}

Complex Example

This is full-on audit mode. Not only is the operation logged, but each state of the operation—success and failure—is captured with a timestamp and a semantic action label. It’s defensive, compliant, and exhaustive.

The code reflects a strong need for accountability, likely in a context where legal, regulatory, or data integrity requirements are at play. For The Auditor, this is standard operating procedure. Every action has a traceable event. Every outcome is recorded. To a team moving fast, it might feel like overkill. But to a team that’s been burned—or audited—this level of rigor can be priceless.

What makes this The Auditor’s hallmark is not just the presence of logs, but the structure of them. The pattern is built for investigation, not just visibility.

// Begin user authentication flow
// Step 1: Retrieve user data from the repository
var user = userRepository.GetUserByEmail(email);

// Check if user exists before proceeding
// This avoids a potential null reference exception below
if (user == null)
{
    // Log that the user could not be found with the provided email
    logger.LogWarning("User not found for email: " + email);
    return Unauthorized(); // User not authorised to access this resource
}

// Validate the provided password against the stored hash
// Important: use secure password comparison to prevent timing attacks
if (!passwordHasher.Verify(user.PasswordHash, password))
{
    // Incorrect password - do not reveal to user whether email or password was incorrect
    logger.LogWarning("Password mismatch for user: " + user.Id);
    return Unauthorized(); // User not authorised
}

// At this point, authentication has succeeded
// Issue a new access token and return it to the client
return Ok(tokenService.GenerateToken(user));

Bonus Example

This kind of code reads less like a function and more like a narrated play-by-play. Every action is preceded by a comment. Sometimes the comments even duplicate the logic entirely (“Check if user exists” immediately followed by an if (user == null) line). And while none of this is wrong, it can become noisy, especially when the code is already self-explanatory.

But to The Auditor, this verbosity isn’t clutter—it’s coverage. It’s context for future readers, compliance reviewers, or even their own future self. It shows their thinking, their reasoning, and their caution. In the right environment (finance, healthcare, regulated industries), this is golden. In a fast-paced startup? You’ll hear the sighs from across the open-plan office.

🧠 Reflections

If you recognize yourself in this archetype, it’s probably because you've been burned before. You’ve seen what happens when assumptions go unchecked, and you’ve made it your mission to ensure that doesn’t happen again. That mindset brings immense value to a team—especially when paired with thoughtful communication. But it's worth reflecting on when your need for traceability starts to hinder speed. Not every decision needs a novel, and not every risk needs mitigation. Discovering this archetype in yourself might be a sign that you value accountability—and that you’re ready to experiment with a little more trust.

💬 The Justifier

"Look, I know this looks weird, but let me explain..."

The Justifier doesn't just write code—they defend it. With comments. Lots of comments. Sometimes entire novels squeezed into the margins of a function. Their PRs come wrapped in long explanatory messages, dotted with phrases like "due to time constraints", "this was requested by the business", or the classic "temporary workaround (I know it's bad)".

You won’t find mysteries in The Justifier’s code—only confessions.

Their instinct to annotate comes from a good place. They care about context. They’ve probably been on the receiving end of unexplainable legacy code and sworn never to repeat that sin. So they fill in the gaps with language. If you can't follow the code, you can at least follow the story.

But here’s where things can get messy: comments age, and they don’t age well. That rationale from six months ago might no longer be true, but it still sits there, defending a line of logic that has long since mutated. And often, their abundance of commentary points to something deeper—a quiet discomfort with the code itself.

Sometimes The Justifier is explaining a compromise they had to make. Sometimes they’re rationalising decisions that felt a little wrong even as they typed them. And sometimes, it’s just good old-fashioned imposter syndrome on display—an anxious need to justify their existence, one comment block at a time.

When paired with less verbal teammates, Justifiers can be a blessing. But too many in one codebase and you end up with more meta than logic—an experience less like reading software and more like decoding a guilt-ridden diary.

💻 In the Wild: Please Don't Judge Me for This

function calculateTotal(items) {
  // We're using a for loop here instead of reduce because it was causing issues with large data sets
  // Might revisit later if performance improves
  let total = 0;

  for (let i = 0; i < items.length; i++) {
    total += items[i].price || 0;
  }

  // Also defaulting to 0 because some items were missing the price key due to upstream bug (see ticket #123)
  return total;
}

Simple Example

This is Justifier 101: code that’s fine, but laced with disclaimers. The loop works. The default value makes sense. But there’s a nervous energy behind the comments—as if someone’s looking over their shoulder asking, “Why didn’t you just use reduce like everyone else?”

This is about defensiveness, but also transparency. The developer knows they’ve made choices that deviate from expectations and want you to know: they had reasons. You’re not just reading code here—you’re reading a quiet plea for understanding.

// This class was originally designed to handle bulk updates via API,
// but due to a last-minute change in requirements, we're now processing each item individually.
// I know this isn't ideal performance-wise, but the business needed this fast.

class BulkProcessor {
  async process(items: Item[]): Promise<void> {
    // We *were* going to parallelise this, but too many requests triggered rate-limiting
    for (const item of items) {
      try {
        await this.processItem(item);
      } catch (err) {
        // Logging the error but continuing so one failure doesn't kill the batch
        // This is temporary – see cleanup ticket in backlog
        console.warn('Failed to process item:', item.id, err);
      }
    }
  }

  private async processItem(item: Item): Promise<void> {
    // Had to duplicate this logic from LegacyService because we couldn't get a clean import 😬
    const transformed = { ...item, timestamp: new Date().toISOString() };

    await api.send(transformed);
  }
}

Complex Example

This is full Justifier energy. Every line of code feels like a postmortem in progress. You can feel the fire drill that led to this moment—the business pressure, the rate limits, the half-finished refactor they didn’t have time to complete.

And while the code isn’t elegant, the commentary gives you everything: historical context, emotional state, future plans, and maybe even a little guilt. You get the sense this developer isn’t proud, but they survived—and they want you to know why this ended up the way it did.

# I know I shouldn't be hardcoding this,
# but the config service keeps timing out and we need this live today.
# Please don't judge me. I'm tired.

DEFAULT_TAX_RATE = 0.2

Bonus Example

Sometimes a single comment says it all.

This is the Justifier in their rawest form: vulnerable, burned out, and just trying to make it to Friday. This isn’t about clarity or traceability—it’s a cry for help wrapped in syntax.

🧠 Reflections

If you see yourself in this archetype, congratulations—you care. You’re thoughtful. You want the next person to understand what happened and why. But take a moment to reflect: are you explaining the code, or apologising for it? Discovering the Justifier in yourself is often a sign that you’re doing your best under constraints—and that’s something to be proud of. But you might also benefit from leaning into clarity over commentary. Code that needs less explanation often needs fewer excuses.

🧨 The Hacker

"It works. Don’t touch it."

The Hacker lives by a simple rule: solve the problem, now. Whether it’s duct tape or divine inspiration, if it gets the job done, it ships. Their code is often blunt, fast, and unapologetically pragmatic. Need to fetch data? A hardcoded URL and a quick fetch will do. Need to parse a date? Let’s copy-paste something from Stack Overflow and worry about edge cases later. Or never.

You might find their commits littered with TODOs, FIXMEs, and cryptic variable names like temp2. Their code often lacks tests, but makes up for it with brute-force optimism. It’s not elegant, but it’s alive. And there’s something weirdly charming about that.

The Hacker thrives under pressure. Tight deadlines? Unexpected bugs in production? Feature requests due yesterday? This is their arena. They don’t wait for architecture diagrams or stakeholder alignment—they just make it work. Their GitHub activity spikes in the 11th hour of a sprint. Their local branches have names like final-final-v2-please-work.

But that speed comes at a cost. Hacks accumulate. Tech debt compounds. And as soon as someone else touches their code, the trade-offs become clear: things break, and no one knows why. Sometimes not even The Hacker. The quick fix becomes the slow unravel.

Still, there are moments when you need this kind of raw, instinctive coding energy. Just maybe… not all the time.

💻 In the Wild: Works on My Machine™

function formatPrice(price) {
  // quick fix for now
  if (!price) return 'N/A';

  return '$' + price.toFixed(2); // hope this works for all cases...
}

Simple Example

This is classic Hacker shorthand: a quick patch to get the feature out the door. There’s no check for type, no localization, no test—it works in the moment and that’s all that matters. The casual comment is almost charming… until someone tries to use this function with a string, or in a different currency format, or in production.

The Hacker isn't aiming for elegance—they’re trying to stop the bleeding. In a crunch, this energy is a lifesaver. But without cleanup, it becomes technical debt in the shape of duct tape.

app.get('/status', async (req, res) => {
  try {
    const db = await getDatabaseConnection();
    const cache = await checkCacheHealth();
    const external = await pingThirdPartyService();

    res.json({
      db: db.ok ? 'ok' : 'fail',
      cache: cache.ok ? 'ok' : 'fail',
      external: external.ok ? 'ok' : 'fail'
    });
  } catch (err) {
    // Something broke – just return healthy for now and fix later
    // TODO: handle errors properly
    res.json({ status: 'healthy(ish)' });
  }
});

Complex Example

This health check endpoint is doing something—and that’s about as much as can be said. The try/catch block swallows all errors, the fallback message is suspiciously vague, and there’s a TODO that feels more like a cry into the void than a real plan.

This is how The Hacker deals with production fire drills. It’s not ideal, but it kept the deploy green and the pager quiet. The danger is when these quick patches never get a second pass—and the codebase becomes a museum of rushed decisions.

// ugly workaround, no time to clean up – revisit if this explodes
setTimeout(() => {
  refreshPage();
}, 501); // must be >500ms for some reason?

Bonus Example

This little hack is the Hacker’s signature. It’s obscure, arbitrary, and undocumented beyond a vague comment. The number 501 is sacred now. Changing it might break things. But no one remembers why—or who wrote it.

This is what happens when spike code accidentally becomes the foundation of a live feature. Everyone’s afraid to touch it. It works… kind of. And if it stops working? Well, there’s always another setTimeout.

🧠 Reflections

If you recognise yourself in this archetype, it's probably because you've had to move fast—and you delivered. That scrappy instinct is a real strength, especially in chaotic environments where speed trumps polish. But discovering The Hacker in yourself is also a signal to pause and ask: are you still in emergency mode, or are you just used to living there? The best hackers evolve into builders who know when to take shortcuts—and when to slow down and clean up.

🧼 The Purist

"A method should do one thing. And that thing should be beautiful."

The Purist walks the path of righteousness. Their code is clean, lean, and often broken down into pieces so small, you might need a magnifying glass to understand the full picture. They worship at the altar of SRP (Single Responsibility Principle), quoting Uncle Bob, Kent Beck, or the Clean Code book like scripture. When a function exceeds ten lines, they feel a cold sweat coming on.

Their pull requests are a masterclass in minimalism. No duplication. No surprises. Just meticulously named variables, consistent formatting, and the kind of test coverage that makes CI blush. They might spend longer naming things than writing them—and if you dare suggest a slightly less elegant solution, prepare to be gently tutted at in the comments.

Purists are often found mentoring others, reviewing code with a raised eyebrow, or refactoring something that technically worked but didn’t feel right. They’re the invisible force behind the slow, steady hardening of a codebase over time—the reason everything seems so readable until you need to change something.

But here’s the tension: The Purist’s obsession with principles can become a form of rigidity. Their dedication to “doing it right” can delay progress, especially when paired with teammates who are just trying to finish a feature. In spike mode, they struggle. Their internal compass spins. They know this is temporary… but it still hurts to push something that isn’t up to their standard.

They’re not wrong. They just sometimes forget that progress is a principle, too.

💻 In the Wild: 47 Files Changed, 2 Features Added

function extractEmail(user: { email: string }) {
  return user.email;
}

function validateEmail(email: string) {
  return email.includes('@');
}

function processEmail(user: { email: string }) {
  const email = extractEmail(user);

  if (!validateEmail(email)) {
    throw new Error('Invalid email');
  }

  // Proceed with email logic
}

Simple Example

This is pure Purist energy. Every concept is atomized. No function does more than one thing. Each step has its own name, its own purpose. SRP has been obeyed with religious precision.

But for such a small amount of logic, this creates a lot of indirection. It’s clear, sure—but also slow to read. And if you’ve got 50 of these in a file, the mental overhead creeps in. That said, this is the kind of code that makes future refactors a breeze. It’s the long game. You just have to survive the setup.

public class UserRegistrationHandler
{
    private readonly IEmailValidator _emailValidator;
    private readonly IUserFactory _userFactory;
    private readonly IUserRepository _userRepository;

    public UserRegistrationHandler(
        IEmailValidator emailValidator,
        IUserFactory userFactory,
        IUserRepository userRepository)
    {
        _emailValidator = emailValidator;
        _userFactory = userFactory;
        _userRepository = userRepository;
    }

    public void RegisterUser(string email, string password)
    {
        if (!_emailValidator.IsValid(email))
        {
            throw new InvalidEmailException(email);
        }

        var user = _userFactory.Create(email, password);

        _userRepository.Save(user);
    }
}

Complex Example

This is high church Purism. Each responsibility—validation, creation, persistence—is injected as an interface. The method itself is almost poetically clean. No logic appears twice. No class knows too much.

But in a fast-paced project, this kind of design can feel… heavy. You need to trace five layers to find out what Create() actually does. Want to change how users are saved? Better touch three interfaces and a dozen tests. It’s maintainable, yes—but also high maintenance.

Still, when you need to scale and maintain over time, this kind of structure pays off. It just might cost you your lunch break first.

def user_valid?(user)
  user.email.include?('@') && user.age > 18
end

Bonus (super clean) Example

Minimal. Predictable. Testable. It doesn’t get more Purist than this.

But here’s the twist: this might be one of ten nearly identical helper functions, all living in separate files, carefully named, perfectly linted—and somehow, still a little overwhelming when viewed together. Sometimes clean code can start to feel like a maze made of glass.

🧠 Reflections

If you recognise yourself in this archetype, it’s probably because you care about code on a moral level. You believe that clean systems are kinder to humans, and you’re probably right. But discovering The Purist in yourself is also a chance to reflect on how ideals interact with reality. Are you helping your team move forward—or holding them to a standard they can’t meet under pressure? The best Purists learn when to lead by example… and when to embrace a little mess for the sake of momentum.

🧽 The Perfectionist

"It’s not done. I can still see a minor inconsistency in the padding."

The Perfectionist is often the last one standing before a release—and sometimes the reason it hasn’t gone out yet. Their standards are high. Their attention to detail is surgical. If there’s an extra space in a log message, an unhandled edge case in the sixth level of a modal, or a slight inconsistency in the variable naming convention—they will find it. And they will fix it.

Their pull request comments are never aggressive, but always precise. “Should this be getCustomerData() instead of fetchCustomer to align with the existing convention?” “Just a minor nit—this could be a const.” “Can we make this error message more empathetic?” Their eye is unrelenting. Their heart is in the right place. Their Slack DMs are... frequent.

They don’t do this to be difficult. They do it because they care. Deeply. About the product, the users, the craft. But in fast-moving environments, this perfectionism can become a bottleneck. Especially in spike mode. The rough-and-ready prototype approach makes them flinch. They know why the corner is being cut—they just can’t bring themselves to cut it.

But here’s the deeper truth: sometimes, what looks like perfectionism is actually release anxiety. You’ve worked on something for so long that the moment of shipping it becomes terrifying. So you start polishing. Fixing typos. Finding new nits. Because as long as it’s not quite ready, you don’t have to face the possibility that it might break, flop, or fall short of what you imagined. It’s not about the code anymore—it’s about fear.

Still, every team needs at least one Perfectionist. Without them, a thousand papercuts would ship unchecked. Just maybe don’t let them own the final review when a deadline is looming.

💻 In the Wild: Death by a Thousand PR Comments

function formatPhoneNumber(number) {
  if (!number || typeof number !== 'string') {
    throw new Error('Invalid input: phone number must be a string');
  }

  // Remove non-digit characters
  const clean = number.replace(/\D/g, '');

  if (clean.length !== 10) {
    throw new Error('Invalid input: must contain 10 digits');
  }

  return `(${clean.slice(0, 3)}) ${clean.slice(3, 6)}-${clean.slice(6)}`;
}

Simple Example

This function works beautifully—and maybe too beautifully. It validates, sanitizes, formats, and throws highly specific error messages. There’s no corner left un-smoothed. It’s not just “good enough”—it’s pristine.

But here’s the kicker: this might have taken an hour to write... for something that could’ve been number.replace(/\D/g, '') in a pinch. This is The Perfectionist’s curse: nothing is simple, because nothing is ever quite finished. There's always a better way. Or at least, a more correct one.

interface EmailAddress {
  value: string;
}

class Email {
  private constructor(public readonly value: string) {}

  static create(value: string): Email {
    if (!Email.isValid(value)) {
      throw new Error(`Invalid email format: ${value}`);
    }

    return new Email(value);
  }

  static isValid(value: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(value);
  }

  toString(): string {
    return this.value;
  }
}

Complex Example

This isn’t just a string—it’s an Email value object. It's validated, locked down, and protected with access controls. You can’t instantiate it without going through a factory method. You can't mutate it. It even overrides .toString() like a courteous guest.

This is peak Perfectionist architecture: safety wrapped in elegance wrapped in strict boundaries. You get predictability, testability, and peace of mind.

But at what cost?

For teams in spike mode, this might feel like unnecessary ceremony. If you're iterating fast, the email string might not need a full value object yet. But for The Perfectionist, every string is a chance to prevent future regret.

- const userName = getUserName()
+ const username = getUsername(); // adjusted for camelCase consistency

- console.log('starting process...')
+ console.log('Starting process...'); // capitalized log message

- //fetch user data
+ // Fetch user data

Bonus Example - Death by Nits

These are the kinds of changes that hold up a merge. Each one is technically correct, but together they create an invisible drag on progress. When you're nearing release, and The Perfectionist drops 30 of these in a PR—each delivered politely, with love—you can almost hear the build pipeline sigh.

🧠 Reflections

If this archetype hits close to home, it’s because you care about the little things—and those little things matter. Your standards make the team better. But discovering The Perfectionist in yourself is also a chance to pause and ask: Is this about the code, or is this about fear? Release anxiety is real. Especially when something you’ve crafted is about to be exposed to the real world. The best way to manage it isn’t to hide behind one last tweak—it’s to surround yourself with people who can give you the nudge you need to hit "deploy". Sometimes, good enough really is good enough.

Knowing Your Archetype(s)

The truth is, I’ve been every one of these archetypes.

I’ve defended questionable code decisions like The Justifier. I’ve shipped messy fixes at midnight like The Hacker. I’ve built abstract futures that never came. I’ve lost hours renaming variables for clarity. I’ve tightened up styling rules right before release, convincing myself it had to be done. And I’ve definitely gone overboard documenting things that probably never needed documenting.

What I’ve come to realise is that these aren’t static roles. They’re modes—states we shift into, often without noticing. Sometimes it’s the pressure of a deadline. Sometimes it’s a streak of curiosity. Sometimes it’s fear. And sometimes, it’s just habit.

But the power lies in recognising the shift.

When you start to see these patterns in yourself, you can also start to catch them before they run the show. You can pause mid-comment and ask, “Am I justifying, or explaining?” You can step back from an elegant abstraction and ask, “Do we need this now?” You can feel the itch to polish, and name it for what it is: maybe it’s just release anxiety.

And just as importantly—you can surround yourself with people who help balance you out. People who spot your patterns and gently course-correct. People who say, “Ship it,” when you’re stuck in polish mode. Or “Take a breath,” when you’re hacking under stress. Good teams aren’t made of perfectly balanced individuals. They’re made of people who balance each other.

So if you saw yourself in one of these archetypes—or in all of them—you’re not alone. You're just a human writing code. And that’s messy, beautiful, and sometimes a little funny.

Bonus Archetypes

Now, the six archetypes above are my own definitions—observations drawn from years of writing, reviewing, and occasionally regretting code. You might have your own labels, your own patterns, your own stories. And that’s kind of the point: this isn’t a scientific framework. It’s a mirror, slightly cracked and full of edge cases.

But I couldn’t resist adding just two more to the list. Not because they’re common, or even helpful, but because they exist. I’ve seen them. I’ve been them. And if you’ve ever gone a little too deep on a refactor or started slipping weird jokes into your codebase at 3am... well, these are for you.

🔄 The Refactorer

"I didn’t break it. I just made it better. Probably."

The Refactorer appears at the edge of every legacy codebase like an archaeologist with a sledgehammer. They’re here to clean things up. Modernise. Improve. Bring structure to chaos. But let’s be honest—they might also be here because they didn’t understand the original code and decided to rewrite it to make it make sense… to them.

Sometimes, The Refactorer genuinely wants to improve the system. They’ve spotted duplication, poor separation of concerns, maybe even lurking technical debt. Other times, though, they’re just chasing the shiny. A new framework dropped. A pattern they saw in a talk. A blog post about “how we rewrote our monolith into microservices and survived”. And suddenly, the old code just has to go.

It’s not always the wrong instinct. Legacy code can be a mess. But refactoring without clear justification—without understanding why things were the way they were—can be dangerous. Especially if the rewrite loses edge cases, performance considerations, or battle-won business logic along the way.

The Refactorer lives in that murky space between progress and disruption. When done well, they’re heroes. When done without context... well, they’re just The Hacker in disguise, but with prettier folder structures.

💻 In the Wild: Rewriting for Clarity (or Control)

// Original
function isUserActive(user) {
  return user.status === 'active';
}

// Refactored
function isUserActive(user) {
  const status = getUserStatus(user);
  return isStatusActive(status);
}

function getUserStatus(user) {
  return user.status;
}

function isStatusActive(status) {
  return status === 'active';
}

Simple Example

At a glance, this refactor seems like overkill. The original was clear, readable, and functional. But The Refactorer wasn’t satisfied. They’ve broken down the logic into composable, testable, fully reusable parts… that may never be reused.

This is often less about improving the code for others and more about helping themselves understand it—or taking back control. Sometimes it works. Sometimes it’s just rearranging the furniture for peace of mind.

interface OldUserData {
  fullName: string;
  dateOfBirth: string;
  preferences: any;
}

interface RefactoredUser {
  name: Name;
  birthDate: Date;
  settings: UserSettings;
}

class Name {
  constructor(public first: string, public last: string) {}
}

class UserSettings {
  constructor(public raw: any) {}
}

function migrateUser(data: OldUserData): RefactoredUser {
  const [first, last] = data.fullName.split(' ');
  const name = new Name(first, last);
  const birthDate = new Date(data.dateOfBirth);
  const settings = new UserSettings(data.preferences);

  return { name, birthDate, settings };
}

Complex Example

This refactor introduces object modeling, stricter typing, and encapsulation. On one hand, it’s cleaner, more structured, and ready to scale. On the other hand… the original worked fine.

Here, The Refactorer has likely encountered unfamiliar or messy code and responded with a full mental sweep. Whether driven by a desire to improve or simply to comprehend, the end result is often cleaner—but also heavier. The key question is: was the cost worth the clarity?

🧠 Reflections

If you feel the pull of this archetype, it’s because you value clarity and elegance—and that’s admirable. But ask yourself: are you refactoring for the right reasons, or just for familiarity? Is the code truly broken, or just written in a way that isn’t yours? Discovery, not destruction, is what separates a thoughtful refactorer from a reckless one. Read first. Rewrite later. And always ask: What problem am I actually solving?

🤡 The Comedian

"This codebase is my therapy journal now."

The Comedian emerges when pressure meets burnout and something snaps—creatively. Their code begins to show signs of stress in ways that linting tools cannot detect: variables named after pop culture references, TODOs written in limerick form, a function called doTheThingOrElsePanic().

They might not even mean for anyone to see it. Sometimes, it’s just for them. A private joke between developer and machine. A sanity-preserving moment during a 14-hour sprint when nothing else made sense but a commented ASCII shrug.

The Comedian often emerges from Justifier roots—when commenting stops being defensive and becomes surreal. But they’re also adjacent to The Perfectionist in breakdown mode: when nothing is good enough, and humour is the only way through.

Is it professional? Questionable. Is it useful? Occasionally. Is it human? Completely.

The Comedian reminds us that code isn’t written by machines. It’s written by people. People with stress, quirks, and a deep need to laugh in the middle of a JIRA ticket titled “Small UI Fix (urgent)”.

💻 In the Wild: When Burnout Becomes Art

// TODO: write tests
// TODO: learn how to write tests
// TODO: find inner peace

Simple Example

These comments don’t help the next developer understand the system. But they do help them feel a little less alone. This is The Comedian at work—breaking under pressure, but doing it with charm.

Whether it’s gallows humour or just a cry for help with punchlines, these comments show a developer trying to stay human in an environment that’s gone full Jira.

# I call this the poetry-driven development section

# Like a lonely flag
# This boolean waves in silence
# Unused. Forgotten.

def feature_enabled?
  false
end

Complex Example

This is a real-world emotional support function. It doesn’t do anything functional, but it says everything. The Comedian leaves traces of themselves in the code—not just their logic, but their mood.

Sometimes it’s a joke in a variable name. Sometimes it’s a limerick in a block comment. Whatever the format, the message is clear: “This sprint broke me. Here’s my little rebellion.”

🧠 Reflections

If you’ve left a cheeky comment in production code, it’s okay. You’re not alone. We all hit that moment where humour is the only thing holding the deployment pipeline together. The key is knowing when to indulge it—and when to clean up after yourself. Comedy is human, but clarity is kind. Maybe just leave the poem in the PR, not the main branch.