Saturday, 29 August 2026

Getting A Senior Backend Software Engineer Job in the AI Era


Disclaimer: None of this is professional advice. I'm not a professional. I'm just loud about my opinions and occasionally employed.

So you got the "we value your contribution" email.

I've worked at three start-ups that started-up, doing the right thing at the right time, but the right time passed, and every other company was also doing the same thing at the same time. Because I don't have million$ lying around to retire early, I had to scramble for new jobs, and in the process I've made a few notes worth sharing.

Before you even start applying

If redundancy is on the table, don't just take it. Ask if they'll do a pay cut or reduced hours instead, like a four-day week, while you look for something else. It beats watching your savings evaporate while doom-scrolling job boards. They might say no. Doesn't matter. Ask anyway.

And when the "here's your severance" conversation happens, don't just nod and sign. Have an actual conversation with a legal advisor about what you're entitled to before you agree to anything. You can't un-sign a thing.


The market has changed, and not in the fun way

There are many people looking for work right now, and companies know it. They can afford to be picky. I did roughly eight final-stage interviews before I got my current job. Eight. Not eight interviews total. I did eight final stages, the ones where you think you've basically got it.

Here's my working theory on why: I wasn't specialised. I bounced between different bits of tech over the years, so I applied for jobs where I wasn't quite the perfect fit, and companies picked the person whose CV matched the spec exactly. Generalists lose to specialists at the CV-screening stage, every time. You need to look like the answer to their exact question, not a good answer to a slightly different question.

There's an interesting quirk of modern job hunting that seems counter-intuitive. It's obvious that companies will reject you for being too junior, but they also reject you for being too senior, on the theory that you'll get bored and leave in six months. What that means is that, if you're desperate, you may apply for junior roles, with salaries below what you're worth. You're not going to get those... there are thousands applying for those. What companies are mostly looking for are people who can complete tasks and look after software by themselves. Aim for the job you're already doing, or perhaps with a bit more responsibility, even nudging your salary range slightly higher. Employers want people who are worth a lot.

And yes, they will ask "what's your AI workflow?" Many do this! Have a real answer ready. "I asked Claude Code to do it" is not a good enough answer, even if, behind the scenes, you're the Lord of the Bots. You need to be able to explain this well, along with the shortcomings of AI.

See my blog post on AI dev for more nonsense on this topic.

Use AI to prep

You can use a Claude project for this. The project has many files, but the most important is a big document of everything you've ever worked on. Markdown is probably best. Then write prompts that tell the AI exactly how you want it to answer the standard interview questions, using your history and the job spec together. The standard questions are always the same:

  • "Tell me about yourself." They always ask this. Have a template that adapts per job, including a few lines establishing that you've done this exact job before, somewhere else, and you were good at it.
  • "Why do you want to work here?" Not salary, not location. This is about matching your expertise to what they do, and showing you've actually looked at what they do. Somewhere between "obsessed super-fan" and "didn't check the company name" is the target. It's really important to hit the sweet spot here - this was where I was constantly filtered out in the beginning because I was "not enthusiastic enough." When I showed super-fan level enthusiasm, I got the generic "Thank you for your..." When I hit the sweet spot, I started getting interviews. It doesn't actually matter that you're really enthusiastic - you need to explain your enthusiasm in the right way. Their website and job spec can help with that... there is often a careers section where they will tell you why you will love working there.
  • Questions for the interviewer. You have to ask something, at both the hiring-manager stage and the final stage... not necessarily the questions that Claude tells you to ask, but something to show you've been listening. Interrupting the interviewer to ask them about something they've said, because you're genuinely interested, might be better than listing generic questions at the end. I got rejected after a 6 stage interview process because I "wasn't dynamic enough". Ask about the interview process itself while you're at it. Interviewers often give you hints about how to do well. This is gold - make notes! Especially for coding tests - you can actually ask what they're looking for, if they don't already tell you.
  • Research the company first, before any of the above. What they do, what they claim to value. I once didn't get a job offer after doing well up to the final stage, because they asked a values-based question... "Are you the kind of person that invites people to the pub after work?" Had I known their core value was about being a friend, I could have responded, "No, I organize other socials" rather than just "No." If they do ask weird questions you can always ask them what they're hoping to learn from the answer, and then give a proper answer that actually answers the question.

Get the AI to do mock interviews too, ideally with voice mode. Practising thinking on your feet out loud is a completely different skill from writing good answers, and it's the one that actually gets tested live.

While I was doing all this, I got AI to quiz me in a way that it tracked what I knew, so I didn't have to answer the questions to topics I already knew well. I did this by building a website that does it properly: fullmarks.quest. You give it a topic, like a job spec, React, AWS, whatever's making you nervous, and it builds you a course, quizzes you on it, works out exactly where your gaps are, and then uses spaced repetition to make sure the gaps actually close instead of getting quizzed away and forgotten by Thursday.

Don't wait to be asked the right question. Interviewers work off an arbitrary checklist that may or may not cover the interesting parts of you. If there's something worth knowing that they haven't asked about, just tell them.

Your CV needs one thread, not several

The failure mode is a CV that's trying to be all things, like full stack, but backend-leaning, but a React specialist, but open to lead roles, but also individual contributor. Pick the story. Filename, title, and experience section should all point at the same job.

Some specific notes, on a real CV I was sent:

  • If the file's called "Full Stack" but the title says "Backend," that's a fight your CV is having with itself before anyone's even opened it.
  • Titles matter less than you'd think, honestly. I got hired as "senior backend" and my contract says Staff Software Engineer. Nobody's checking.
  • Drop weakening words. "Strong React specialist". "Strong" undercuts "specialist." Either you're a specialist or you're not.
  • Skip "15+ years." Just say the number. The plus is doing something I can't quite name, and I don't trust it.
  • Bold your tech stack in the experience bullets. If someone's skimming, and they are always skimming, they should be able to see "TypeScript, three jobs" at a glance.
  • Find your numbers. Cost cut, time saved, incidents reduced, anything. If you genuinely don't have a number, work backwards and make a reasonable estimate rather than leaving it blank.
  • If your job history has a run of short stints, have a real answer ready for why, because they will ask, and "it just didn't work out" three times in a row is not an answer, it's a red flag wearing a trench coat.

If you're going for lead, prepare differently

Titles are mostly noise, like I said, call yourself senior, lead, staff, principal, whatever fits the spec, nobody's checking your payslip. But the interview for a lead role is a very different interview, and treating it like a slightly-more-senior version of the IC one is how you get caught out.

Almost regardless of the exact title, expect mentorship questions in senior roles. That one's universal. But for anything explicitly "Lead," get ready for a run of questions about how you actually work with other roles and steer a team:

"How did you work with the product owner?" "What's your leadership style?"

These aren't technical questions and you can't wing them on the day. I found them genuinely hard, and needed proper prep time before I was comfortable answering, not because the questions are clever, but because "leadership style" is the kind of thing you've never had to articulate out loud before, even if you've been doing it for years.

As a tech lead I used an approach I called, "The Definition of Awesome". That landed well. Maybe I'll do a blog about it one day.

The stuff you're expected to know cold, backend edition

If you're going for anything backend-leaning, here's the syllabus, apparently:

  • Observability. Name real tools your company used, like Sentry, Prometheus, Kibana, the ELK stack. Know when to log info versus warning versus error, and have an opinion on structured logging. For AWS specifically: X-Ray for tracing, CloudWatch for logs/metrics/alerts. For GCP: Cloud Trace. Know what RED stands for. If someone asks how you'd handle logging, don't say "with a logger". Say whether you use structured logging and what fields you included.
  • REST status codes. All the ones that come up in real arguments: 200, 201, 401, 403, 404, 429, 500. Know when to retry or fail.
  • Microservices, and specifically when you'd reach for a synchronous API call versus an async message-broker-and-handler setup. This comes up constantly.
  • ACID and isolation levels. Fintechs in particular love to test you on "how do you make a safe payment or withdrawal," which is really just a transactions question.
  • System design. This is the one that scared me most. Watch walkthroughs. This playlist got me through it. Practise a handful of designs until you're not improvising from zero, and make sure your DB / SQL choices are solid. You probably won't be asked to write SQL that actually works. Pseudocode is usually good enough, as long as you include your isolation levels and locking.

Or, you know, build a course on it in fullmarks.quest and let it figure out which bits you actually need to drill. It doesn't care that system design terrifies you. It just keeps asking until it stops terrifying you.

Cloud specifically, like AWS or GCP, is mostly a concepts test, not a "write me some Terraform live" test. Nobody's ever asked me low-level infra questions. Have at least one real story about something you actually deployed, and understand the concepts well enough that they transfer between providers, because they mostly do.

One more thing that took me too long to figure out: you don't need to remember the deep details of everything you built. "I don't remember the exact details, but if I were doing it today, here's how I'd approach it" is a completely fine answer. Seniority doesn't mean total recall. It means you know what a good approach looks like even when the specifics have faded.

Where to look

LinkedIn will happily let you apply to hundreds of jobs and reject you from all of them, usually with a template email that manages to say nothing at all. It's possible that you'll get an occasional interview, but it's a low-yield, thick-skin-required strategy, and writing a tailored cover letter for each one, then editing it so it doesn't read like Claude wrote it, gets old fast. After many depressing, "Thank you for your application..." emails, I stopped applying on LinkedIn.

Turning on "open to work" seems to get recruiters coming to you instead, often in a weekly rhythm. Tuesdays, in my experience, for whatever reason. Recruiters who reach out have effectively pre-filtered you against a real role, which is worth something.

My highest probability of success was via people I knew - either former colleagues or recruiters who had great experiences with me in the past. With former colleagues, you can get valuable information, like exactly what they're looking for in the coding test. Better to delay the interview and prepare properly than give up the rare opportunity by handing in a coding test that focused on robustness, when they just wanted performance.

A few other places that produced better results than LinkedIn, for what it's worth:

Pace yourself with the applying, by the way. Blast out fifty in a weekend because you're anxious about the silence, and you'll have fifty responses to prepare for the following week, which is its own kind of overwhelming.

If you take one thing from this: figure out the two or three gaps that are actually costing you interviews. Use a spreadsheet, and track where things are going wrong and right, so you get a vague idea of how things are progressing. Solve any gaps properly instead of vaguely re-reading the same AWS docs page for the fourth time. The tool for that is fullmarks.quest. You get free credits, and if you burn through the credits, tell it I sent you.

Monday, 15 June 2026

Well Structured AI Development Practises

We've all tried AI by now.

Some promote a build as quickly as you can approach, while others look at me in horror, as if using AI extensively melts your brain (it does).
The question is whether you're getting consistent, trustworthy output (the kind you'd stake a production release on) or are confident-looking code that half-works and then breaks something at 4:59pm on a Friday?

There's a gap between teams getting serious results with AI and teams getting occasional lucky ones. The gap isn't which tools they've licensed. It's whether anyone has sat down and thought carefully about how those tools fit together, and made it someone's job to keep that thinking current.

Someone has to own this


Every org needs at least one person (a human person) who goes deep on AI-native development. Not "I've skimmed the documentation" but deep enough to write guidance that stops every other engineer from reinventing the wheel, to maintain the high level context files that teach our agents how the organisation actually works, and to have documented guidance and a considered answer when a new tool appears and everyone looks around waiting for someone else to evaluate it.

Without this person, we get well-intentioned chaos. Everyone experimenting independently. Nobody sharing what works. The wheel reinvented in every corner of the codebase. Coding agents that behave completely differently across repos because nobody's maintaining the context files and they've drifted into contradiction.

One responsible person, the go-to guy, fixes most of that.

Context is the actual product

Prompt engineering is what you do for a single conversation. Context engineering is the discipline of building the right information environment for every conversation. It's the difference between briefing a contractor well and just handing them a laptop and pointing at the codebase.

Imagine hiring a contractor who's genuinely skilled. Worked with a hundred teams, knows the patterns, no ramp-up required. But they've walked through the door cold. They don't know you're on Python 3.14. They don't know the auth layer is a shared service. They don't know you spent six months migrating away from the ORM they're about to suggest. That contractor will make expensive mistakes in the first week, not because they're bad at their job, but because they don't know your job.

Context files are the briefing.

Organised in a hierarchy: org-level at the top (your stack, your architecture principles, the things that don't change project to project), project-level below that, feature or component level below that for focused work. The AI lead owns the top. Everyone else contributes to the levels below.

Treat context as code. Version control it. Put it through pull requests. Iterate on it. Run a retro at the end of each sprint and ask: did the coding agents do anything surprising or wrong? Consider if the answer points to a gap or an error in a context file.

Fix it. Ship the change. Check again next sprint.

A fast workflow for big changes

The default way to work is: open chat window, describe the problem, accept or reject what comes back. That works for small, isolated tasks. See Managing risk at the bottom of the blog.

The following idea comes from https://tylerburleigh.com/blog/2026/02/22/. I don't have evidence that it works perfectly - just sharing what I've found. It's a staged approach with review cycles between stages. Errors caught at planning stage cost almost nothing. Errors caught after they've been faithfully implemented across multiple stages cost quite a lot more. So you front-load the thinking.

Research first.
Before any code is written, the coding agent researches the codebase and / or the web, and writes its findings to RESEARCH.md. You read it. You correct any misunderstandings. Only when that's solid do you move on. Start a fresh session. Model performance degrades noticeably once you've used around 40% of its context capacity. Fresh sessions keep it sharp.

Then plan. The agent reads RESEARCH.md and produces PLAN.md: a breakdown of the work into stages, plus CHECKLIST.md. A separate session then reviews the plan from the perspective of a senior engineer. You read that review. Accept or override its recommendations. Revise until it's ready.

This is the highest-leverage point in the entire process. A bad assumption that survives planning gets implemented faithfully, and you'll discover it somewhere around stage 4, trying to explain why the feature is broken in a way that requires unwinding three phases to fix.

Then implement, one phase at a time. Each phase in its own session. Committed when clean. Reviewed before moving on. Use different models for implementation and review. A model that just wrote the code is not ideal to find problems with it. It'll tell you it's fine.

It's not always fine.

LSP plugins give the coding agent live type information from the IDE, so it catches type errors and missing imports without waiting for the compiler. 

Consider Git worktrees if you're running parallel features: separate agents, separate branches, separate directories, no collision, merge as normal.

When it's running well, consider automating the middle


Once this is working reliably you can remove human involvement from the implementation loop. Not from the edges, but only the middle.

The human still approves the plan before anything is built. The human still reviews the PR at the end. What changes is what happens between those two points: an implementer agent executes against PLAN.md, automated gates run (tests, lint, type checks), a reviewer agent evaluates against the plan, and they iterate until clean. No human required in that loop.

The PR is self-documenting. RESEARCH.md, PLAN.md, and the commit history are the description. The engineer can see exactly what the agent understood, what it planned, and what it did.

Nothing's a black box, because "the AI did it" (which is as tempting to say as eating a warm, golden, honey-glazed doughnut oozing with rich, velvety Marmite custard) is not an acceptable answer in a code review. Not yet, anyway.

Managing risk


Not every change carries the same risk, and it helps to think about this on two dimensions: likelihood of something going wrong, and impact if it does. For likelihood, ask how complex the change is, how well-tested the affected code paths are, and how reversible the outcome is. For impact, ask how many users or services are affected and whether you can recover quickly if something breaks. A rough mental multiplication of the two tells you how much caution is warranted.

When I work in core services in financial applications, I micromanage Claude. What do you do?


Wednesday, 20 May 2026

TDD, or should Claude just write everything?

Let's start with a question you've been avoiding: do you actually do TDD, or do you nod along at the conference talk, then go home and write the implementation first like everyone else? Be honest. The tests aren't listening. The uncomfortable truth is that TDD has always demanded a very particular flavour of masochism: sit down, write code that deliberately fails, resist every instinct to just make the thing work, and trust that the scaffolding of failure will eventually produce something beautiful. Most developers respect this in the same way they respect flossing. And now here comes Claude, ready to help. What could go wrong?

Quite a lot, as it happens, and in a rather specific way. Left to its own devices, Claude will skip the red phase entirely. It's too helpful. You ask it to write a test for a feature that doesn't exist yet, and, like an eager intern who's read ahead, it writes the test and the implementation in the same breath, hands them to you simultaneously, and waits for praise. The tests pass, obviously, because Claude just wrote both sides of the conversation. This is the AI equivalent of marking your own homework, then putting a gold star on it. SD Times put it this way: AI doesn't eliminate TDD, it exposes whether you understand it. Which is a polite way of saying it will cheerfully help you do it wrong if you let it.

The solution is that TDD and AI are actually a spectacular match, provided you hold the leash. The cognitive barrier that kills TDD in practice is that writing a test requires defining the interface, which requires understanding the architecture, which requires mentally sketching the implementation anyway; the circle eats itself before you've typed a single assertion. AI dissolves that barrier completely. Let Claude draft the test, you review it, then let Claude implement against it, one cycle at a time, not in bulk. 

In other words, use this exact prompt:

"Sit. Good boy. Now, write one failing test for the login function. Just one. Don't implement anything. Don't even think about the implementation. I can see your little cursor twitching. Stop that. Write the test, run it, show me it's red, and then sit back down. If you've written only the test and nothing else, you'll get a biscuit."

Or, if you're feeling boring, something like this:

Do TDD, one test at a time, like this:
    - Prefer doing the happy path test(s) first.
      When testing edge cases, test as close to the edge as reasonable.
    - Implement code structures, so types used in tests work, but
implementation does nothing or raises, e.g. a method that raises
an exception
    - Write the test
    - Check it fails
    - Implement the literal minimum fix here.
      If more would be done in a full implementation,
      write a one line TODO in the implementation.    
    - Check it passes
    - Refactor if necessary

Now, some of you will have noticed a tension and are feeling very pleased with yourselves about it. If we're telling Claude to stop after every single test like an overexcited Labrador, when exactly does the plan happen? Surely we want Claude to think before it acts? Yes, probably. The thing is, plan mode and TDD aren't quite solving the same problem (or at least, that's how it seems to me). Plan mode is about what you're building. TDD is about whether you actually built it. One is a map. The other is the slightly neurotic habit of checking you're still on the road every five minutes, which feels excessive right up until the moment you aren't.

The temptation is treating a good plan as a green light to let Claude implement the whole thing in one uninterrupted sprint. My suspicion is that this works right up until it doesn't, and when it doesn't, you're several pull requests deep into something that's coherent, confident, and subtly wrong in ways that are deeply tedious to unpick. The plan tells Claude where it's going. TDD, is just the thing that keeps it from lying to you about whether it got there. You probably want both: plan mode to decide what you're building, TDD to keep Claude honest while it builds it. But I'll admit I'm still working out exactly where the seams should be, and anyone who tells you they've fully figured this out is either very clever or writing a different blog.


Thursday, 17 February 2022

How to reduce complexity and know enough.

Quality


We have a great QA.

His code is like a Rolex.

You look at his gherkin, and smile.  It says what it's testing, in plain English, and there's nothing else on the page to confuse or distract you... just English.

"Why then, did our unit tests give me a feeling of dread?"  I asked myself.  It was quite obvious, that even though we used a neat, little NuGet package, called BDDfy to allow us to write English gherkins, our code was littered with private methods and other necessary evils:

private readonly Subject _subject;

MyTestClass()
{
_subject = new Subject();
}

[Fact]
public void WhenNoOneReadsYourBlog_WriteABunchOfCrap()
{
this
.Given(_ => _.NoOneReadsYourBlog())
.When(_ => _.YouFeelLikeWriting())
.Then(_ => _.JustWriteABunchOfCrap())
.BDDfy()
}

private async Task IAmADumbAnnoyingPrivateMethodThatNoOneWantsToSeeAsync()
{
var IMyInterface mockableMockObj = Substitute.For<IMyInterface>();
mockableMockObj.SomeStupidMethodThatNoOneReadingTheTestCaresAbout
.ShouldReturnSomethingElseInsteadOfNotDoingSomethingElse();

for(var i in someStupidComplicatedIteratableThing)
{
await WhyAmIEvenHereAsync();
}
}

"If we could just hide all the non-gherkin stuff, that would make the code more pleasing to read, understand, and work with," I thought to myself.  So I suggested to my team that we use a partial class.  We put the public methods, with the English definition of the tests in one file, and everything else in another.  The team loved the idea, so that is what we do.

Now, we still have many tests in a file (See how I organize unit tests here).  So, the question is, are the tests still useful, to understand what the system does?

Let me put it another way... how do you know enough?


Simplification

The company I work for, IHS Markit, is merging with S&P Global.  The combined company will have a hundred gazillion staff members, but only 6 divisions.  How do you know what S&P do?  You list the 6 divisions.

The universe consists of 200,000,000,000,000,000,000,000 stars.  Perhaps it can be summarized as a bag of galaxies.

It takes many years to learn about the human body, but you can summarize it as five things: head, neck, body, a pair of arms and a pair of legs.

Simplifying complexity into a tree, where each node has about 5 children, makes everything easy to understand.

Microservices is the modern, cool way to develop distributed systems.  AWS S3 consists of 300 services.  I haven't seen this architecture, but I imagine if they're just a list of services that call each other higgledy piggledy, then AWS has a big problem.  Our software has 30 services, and even that is a lot to try to understand.

My point is, that I think pretty much everything in software, and perhaps even life, needs to be broken down into a tree, limited to about 5 child nodes per parent to make it easy to understand.  Now, don't get me wrong... 5 is more of a thumb suck than anything else, but 5 is a handful.

You may be wondering, well, what if it can't be broken down?

  • Maybe I have a property for each country... well, that's just a bag, collection, list, or array.
  • Maybe my system just has 10 things it needs to do... categorize them... facilities, business logic, etc.
My hypothesis is that any group of items can be broken down into smaller groups... it just takes a bit of time to think up suitable categories, and re-organize things.


Code Organization

Problems with flat, rather than code organized in hierarchies, adding to complexity are:

  • Private methods, private fields and private classes clutter the code, making it more difficult to understand than it needs to be.
  • Long methods.

Normally we wouldn't organize classes in a flat structure.  We'd use a hierarchy of folders.  I think it would be interesting if a class or namespace was a folder.  Perhaps an IDE plugin could be written.  It might look like this:

MyApp.csproj
    Program.cs
    Utilities.cs
        DataTypes.cs
            Strings.cs
            Dates.cs
                Conversion.cs
                Validation.cs
        Files.cs
            Encryption.cs
            Persistence.cs
        


Contemplation

Consider code within classes too.

Would limiting methods to 5 per class or interface help to make it easier to understand?  The interface segregation principle suggests keeping interfaces "small" to prevent having classes that have to implement methods unnecessarily.  It may require a little less thought, to have alarm bells going off in your head when you reach 5 methods, to consider whether another interface is required.

Can limiting properties to 5 per class make them easier to work with?  I've seen some pretty large view models.  Code becomes much cleaner when they're broken down into a tree structure.  It can be tricky to change it later, so best to keep them small from the start. 

Can limiting lines to 5 per method make the logic easier to follow?  Uncle Bob says, "extract, extract, extract."  If you follow that, your methods will be pretty small.  Using the number 5 might be too artificial, but when you have to scroll to see your whole method, you've probably gone way too far.

I've heard a suggesting that methods should only have 3 parameters.  The suggestion is that if there are more, they should be in a class.

Disclaimer:  As I've only thought of  some of these ideas recently, I haven't experimented with these ideas very much.  So, I'm going to try them out, and see how it goes.

Thoughts?


Final Thoughts

A tree with a single root, and 5 children per parent, and a depth of 11 nodes, would have just under 10 million leaf nodes.  That's a hell of a lot of complexity, but simple enough to navigate.

There's no need to learn everything.  That's impossible.  I think the most important things to learn, as a programmer, are the capabilities of tools.  That way, whatever you need to build, you can pick the right tool, and then you just need to learn how to use it.  Navigate the tree of knowledge to the depth you need at the time.

Image credits:

https://www.freeimages.com/photo/skeleton-pocket-watch-back-1637098

https://www.freeimages.com/photo/nebula-space-astro-photo-astronomy-sky-1420873

https://www.freeimages.com/photo/burning-tree-1377053

https://www.freeimages.com/photo/dave-in-window-1553933




Sunday, 17 October 2021

Picking a Better Data Store


I like Entity Framework.  It's like arriving at Hogwarts and trying out your wand for the first time .  Sometimes you wave it, and it transforms your rat into a perfect water goblet.  Other times it just blows up in your face, and you have to spend the rest of your high school years trying to master the correct incantation.

The question is, "Why do you have to have magic to transform your code into SQL?"  I used to say for unit testing, but if I want to test my units (whatever a unit is), I could just run them against an in-memory database.  Why do we need to transform a rat into a water goblet, when we could just go out and buy a water goblet, and keep our pet rat?  A rat is not a water goblet, neither is my Linq code TSQL, nor is my aggregate a table.

I use Entity Framework for two reasons: because it's fun, and to show off.  You could say that it makes the code more interesting.  It reminds me of AutoMapper... a library that automatically copies data from one object to another, if the property names match.  If they don't... you can extend your mapping to cater for it.

This is boring:

A.X = B.X;
A.Y = B.Z;

This is cool:

AutoMapperCadabra(A, B);
// TADA!

However, one is easy to debug, and one is not.  That could be a bumper sticker, "Magic... it's not easy to debug!"

Anyway... what was the point of this blog again?  Ah yes... picking a better data store.  What if there was no need for magic?  What if you could just put your aggregate in a database?

And that's why we use document databases.  But there's more to it:

Document databases are typically designed to scale horizontally, splitting the data up in shards on separate nodes.  A relational database typically requires either vertical scaling, or to replicate the entire database to multiple read nodes.

In a document database your aggregate is read and written sequentially... everything for your aggregate is in the same place.  It's not necessary to find bits and pieces all over the hard drive and bundle them together.

But... relational databases have their place.  You can index almost any piece of data, find it, and discover everything it's related to.  Want to find out something complex, like all the brands of wands used to create water goblets where the rat tail was still protruding from the water goblet?  You can!  Not so quick with a document database, unless you read everything.

They also have a schema, meaning that the format of every row is consistent.  This could be important, knowing that whenever you load a record, it it will be in the expected structure.

So, it depends what you need.  You could always dump your changes into a couple of queues and have multiple consumers build up different kinds of data stores, if you really want to.

Tuesday, 27 July 2021

SOLID

Nerdy Monk with Kittens

SOLID
, YAGNI and DRY are acronyms commonly used to describe good programming practices.

DRY is not WET

DRY
is my favourite: Don't Repeat Yourself... instead of writing the same code twice, wrap it up in a function and put it somewhere where it can be easily found and re-used.  This also means if you have to fix a problem, or improve your code, you'll only have to make your change once.

I tend to take DRY to the extreme, by writing little tools, either in PowerShell, or Python, or a little desktop app that takes care of any monotonous task that I might have. The tasks are usually things to do with my dev environment, like automatically powering up useful AWS resources when I login to Windows, and adjusting settings in JetBrains' Rider, so that it only has to compile the bare minimum it needs in order for me to debug the code I'm working on. Other tools I've written include swapping in and out fake and real libraries that my code connects to, so that I can debug under a variety of conditions. These tools not only save time, but allow me to focus on what I enjoy, and what really matters.

DROP is WET

I'd love to invent my own acronym here, DROP: Don't Repeat Other People.

Whenever writing your own generic code, there should be a few things that come to mind... Why has no-one else written this before? Oh, maybe they have. Is there a NuGet package I can reuse? Because, really, if it's generic, and your code is better than any other open source library out there, perhaps you should be either creating, or contributing to open source.

YAGNI... unless it's a kitten!



YAGNI means You Ain't Gonna Need It.  The theory is that your code will be simpler if you delete (or don't write) code that you may need in the future.  It also means you don't have to test it.








SOLID is five principles:

Chaos from multiple responsibilities

1. Single Responsibility: 

A class should only have one responsibility.

Example:  A Person class should not contain code that ensures the email address is in a correct, standard email address format.  Email validation is generic and should go elsewhere.

2. Open / Closed Principle:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

To quote Robert C. Martin (Uncle Bob): 

When a single change to a program results in a cascade of changes to dependent modules, that program exhibits the undesirable attributes that we have come to associate with “bad” design. The program becomes fragile, rigid, unpredictable and unreusable. The open-closed principle attacks this in a very straightforward way. It says that you should design modules that never change. When requirements change, you extend the behavior of such modules by adding new code, not by changing old code that already works.[1]

This means, if you have created a class that is being used, don't make changes unless you're fixing bugs.  Inherit from it instead, and make the changes there. Of course all the cool kids are preferring composition over inheritance these days, so adding a property that's an object is fine, but work with abstractions. When I do this, I might use an interface like IContainX, and then implement a new interface IContainY. That way, your object can be extended with any existing code being referred to (X) remaining unchanged.

If you used TDD properly to build the class, and you're not actually changing the behaviour... You can make performance improvements as long as your original tests pass.

3.  Liskov Substitution Principle:

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

So, if the Tesla class inherits from the Car class, you might not want to have a method Car.FillUpWithPetrol(), or you should simply not create a Tesla class inheriting from the Car class.

4.  Interface Segregation Principle:

Many client-specific interfaces are better than one general-purpose interface.

The reasons:
  • Because any implementation should ideally implement every member.  When testing, and creating fakes, mocks or stubs, we don't want to have to care about whether or not a member is implemented if it's not related to the test.
  • Unnecessary complexity (See my blog post on reducing complexity).  Not everyone who flies in a plane wants to see all the controls in the cockpit of a Boeing 747.  All they may need is a button to call the flight attendant.

5.  Dependency inversion principle:

Depend upon abstractions, rather than concretions.

This is often done using dependency injection. It's a way of loosely coupling objects. You don't solder wires from a lamp into a wall socket... you create interfaces, which both the lamp and source of electricity can use.

The way SOLID's used is a bit funny.  I say it's funny, because when I interview someone they usually remember SINGLE RESPONSIBILITY and waffle through the rest. Yup, we're only human. I've heard the word SOLID used a few times, when the speaker's intention was to refer to single responsibility. I don't think it really helps to ask about it at interviews, directly. It's possible that a good answer just shows that the person's attended many interviews. Perhaps a better test would be to offer some code, and ask what the interviewee would change about it. Although, when I've done that in the past, I tend to get a lot of silence... so who knows?

References:

Saturday, 23 March 2019

Microservices in a nutshell

 (or how to build a high performance distributed application / website that is easy to maintain)

Microservices at a high level is about splitting code horizontally based on context specific data models.  A single application could therefore consist of many services, each with its own business logic, data model and database.

There are drawbacks to creating Microservices.  The problem with popular patterns, is it's easy to consider them to be the right way to do things, but often the best approach is determined through experience, and considering what is really important for the specific context.  In many scenarios you will achieve better performance running code on a single computer, and you'll be able to develop it faster.  Nevertheless, for when it is a good approach, here are some advantages of splitting code into Microservices:

  • All words in a model have a single meaning.  This avoids confusion and complexity which could come about from a large model.
  • Less chance of code conflict: Development teams can focus on single services, without writing code that conflicts with teams working on other services.
  • Services fail independently, rather than the entire system going down.
  • Services can be written in different languages.
  • A single service can be updated without affecting anything else.
  • Enables continous delivery: services can be released independently when a feature is done, without waiting for a feature in another service.
  • Easy to understand, as a small piece of functionality.  New developers do not need to understand the entire application.
  • Scalability - easy to scale and integrate with 3rd parties.  Components can be spread across multiple servers.
Microservices can use event sourcing and CQRS to have optimized databases, so that any views which require extremely fast queries can have denormalized copies of the data, formatted specifically for their associated queries.

Event sourcing is a design pattern where changes to an application's state are stored as a sequence of immutable events, rather than directly modifying the current state. These events represent each action taken, allowing the system to recreate the current state by replaying the event log. This approach provides a complete audit trail and enables easy reconstruction or rollback of past states. It is commonly used in scenarios that require high levels of traceability and consistency.  A well known example of event sourcing is most source control software, which record the changes, rather than the state.

Some benefits of event sourcing are:

  • It provides a 100% reliable audit log of the changes made to a business entity.
  • One can get the state of an entity at any point in time by replaying events.
  • Communication between services without too much concern about a service being unavailable.
CQRS stands for Command Query Responsibility Segregation.  CQRS means that instead of CRUD, where the same entity is updated via the same interface, the command to update the entity can be applied to a different model to the query to retrieve the information.

Some benefits of CQRS include:
  • One can create a view database (readonly) combining everything for a particular UI view.
  • Supports multiple denormalized views that are scalable and performant.
Microservices can be deployed in containers.

Some benefits of containers:
  • Platform independence: Build once, run anywhere.
  • Much smaller than VMs.
  • Effective isolation and resource sharing.
  • Speed: Start, create, replicate or destroy containers in seconds.
  • Smart scaling: where you only run the containers needed in real time.
  • Simpler to update OS for all containers than multiple VMs.
  • Free from issues associated to environmental inconsistencies.
  • Roll-out & roll-back with zero downtime.

Thursday, 14 March 2019

High Precision Timing in SQL Server

Here's a simple snippet I wrote for high precision timing in SQL:

DECLARE @startTime datetime2;
DECLARE @endTime datetime2;

DECLARE @counter int = 0;
DECLARE @iterations int = 1000;

SET @startTime = SYSDATETIME();

WHILE @counter < @iterations
BEGIN
-- code to time here

SET @counter = @counter + 1;
END;

SET @endTime = SYSDATETIME();
SELECT DATEDIFF(MICROSECOND,@startTime,@endTime) / @iterations

Friday, 12 August 2016

Well Organized Unit Tests

Some people write unit tests by copying all of the arrange code to every unit test, so they will end up with code like this:

[TestMethod]
public void MyFunction_with_some_condition_does_something()
{
    // Arrange
    // 20 lines of setup code
    ...

    // Act
    ...

    // Assert
    ...
}

[TestMethod]
public void MyFunction_with_some_other_condition_does_something_else()
{
    // Arrange
    // The same 20 lines of setup code as above with one minor modification
    ...

    // Act
    ...

    // Assert
    ...
}

Of course the problem with this is that if you change one thing in your method's implementation, you may need to change every unit test, and it can get messy.  This makes people lazy, and they will neither want to make changes, nor fix their unit tests.  I'm not sure why people forget about good programming principles like DRY when it comes to unit tests.  So you could do something like this, and keep your unit tests small (3 or 4 lines each), easy to read and change:

private MyFunctionRequest myFunctionRequest;

private void Arrange_MyFunction(bool condition1 = false, bool condition2 = false)
{
    myFunctionRequest = new MyFunctionRequest
    {
      ...
    };
    if(condition1)
    {
      ...
    }
}

[Test]
void MyFunction_with_some_condition_does_something_else()
{
    // Arrange
    Arrange_MyFunction(condition2:true);

    // Act
    target.MyFunction(myFunctionRequest)

    // Assert
    ...
}

This looks okay, until you start to work with multiple methods in a class and find that you have to split your test class into regions and you have member variables that only pertain to a specific region.  Using regions is often an indication that your class is doing more than it should.  A better way to organise your tests is to have one class for each method being tested, like this:

[TestClass]
public class MyServiceTests
{
    protected MyService target;

    [TestInitialize]
    public void Init()
    {
        target = new MyService();
    }

    [TestClass]

    public class MyFunction_Method: MyServiceTests
    {


        private MyFunctionRequest request;



        private void Arrange(bool condition1 = false, bool condition2 = false)
        {
            request = new MyFunctionRequest
            {
                //...
            };
            if (condition1)
            {
                //...
            }
        }

        [TestMethod]

        public void With_some_condition_does_something()
        {
            // Arrange
            Arrange(condition1:true);

            // Act
            var actual = target.MyFunction(...);

            // Assert
            ...
        }

        [TestMethod]

        public void With_some_other_condition_does_something_else()
        {
            // Arrange
            Arrange(condition2:true);

            // Act
            var actual = target.MyFunction(...);

            // Assert
            ...
        }
    }
}

If your method being tested is particularly complicated, you could break it down even further, like this:

[TestClass]
public class MyServiceTests
{
    protected MyService target;

    [TestInitialize]
    public void Init()
    {
        target = new MyService();
    }

    [TestClass]

    public class MyFunction_Method: MyServiceTests
    {
        private MyFunctionRequest request;

        [TestInitialize]
        public void Init_MyFunction_Method()
        {
            request = new MyFunctionRequest();
        }

        [TestClass]
        public class With_some_condition: MyFunction_Method
        {
            [TestMethod]
            public void Does_something()
            {
            }
        }
    }
}

In Visual Studio, your solution explorer will look like this:


Getting A Senior Backend Software Engineer Job in the AI Era

Disclaimer: None of this is professional advice. I'm not a professional. I'm just loud about my opinions and occasionally employed. ...