Arrange Act Assert

Jag Reehals thinking on things, mostly product development

The package manager settings that would have blocked the Axios attack

07 Apr 2026

On the 31st of March 2026, attackers hijacked an npm maintainer account and published malicious versions of axios with a remote access trojan baked in. npm pulled the bad releases after about two or three hours, but that was enough. Anyone who ran npm install axios during that window could have installed the trojan. The article Post Mortem: axios npm supply chain compromise has all the details.

This kind of attack keeps happening and the playbook barely changes: compromise an account, push a malicious update, hope people install it before anyone notices, get removed a few hours later.

Every major package manager now lets you defend against this. In this post I'll show you the setup for npm, pnpm, Bun and Yarn.

Read More →

Engineering decisions are bets, not proofs

06 Apr 2026

Most engineering decisions are not proofs.

They are bets.

You choose a direction with incomplete information, under time pressure, and with trade-offs you cannot fully test in advance.

That does not make the decision weak.

It makes it real.

Engineering decisions are bets, not proofs

In a previous post, I wrote about how design by committee leaves engineering change unfinished. The deeper reason is simple: many organisations treat technical decisions as though they should be certain before the work begins. But most meaningful engineering decisions are not certainties. They are bets.

Read More →

Why design by committee leaves engineering change unfinished

05 Apr 2026

Engineering change often gets stuck for the same reason: design by committee. Not because anyone has bad intentions, but because the pursuit of alignment quietly replaces the pursuit of learning.

Why design by committee leaves engineering change unfinished

It usually starts with good intentions. People want alignment, consistency, and lower risk. So a proposed change gets pulled into more meetings, more reviews, more stakeholders. Before long, the goal is no longer to try something, learn from it, and improve it. The goal becomes finding one perfect solution for every team.

That is usually where momentum dies.

Read More →

fn(args, deps) Is Not Anti-Mock

23 Mar 2026

People hear "don't use vi.mock" and assume the alternative is "don't mock anything." That is not the argument.

fn(args, deps) is not anti-mock

The real distinction is simpler: mock explicit collaborators, not implicit imports.

Read More →

Before You Change the Process, Understand the Problem

20 Mar 2026

Strong opinions should be earned, not borrowed. If you want to propose a change, do the work first.

Understand the trade-offs before you walk into the room.

Before You Change the Process, Understand the Problem

I see the same pattern again and again in software teams. Someone reads a blog post, watches a conference talk, picks up a new buzzword, or sees how a company like Spotify talks about working, and by the next meeting they are proposing that the whole team change how it works.

The idea often sounds compelling at first. But the moment you ask about the trade-offs, the case falls apart. There is no real research, no serious risk assessment, and no clear plan for what to do if the change creates new problems rather than solving the old ones.

That gap between enthusiasm and understanding is where teams get into trouble.

Read More →

What `fn(args, deps)` actually gives you in DDD-style TypeScript

18 Mar 2026

Domain-Driven Design comes with a lot of vocabulary: aggregates, repositories, domain services, bounded contexts, ubiquitous language, anemic models.

That vocabulary can make DDD sound heavier than it really is.

The useful idea is simpler: keep domain behavior and domain boundaries central, and keep infrastructure, persistence, and framework wiring secondary.

fn(args, deps) does not do that modeling for you. What it gives you is a clear shape for application-layer code in TypeScript: one place for use-case input, one place for collaborators, and less room for domain decisions to drift into wiring.

fn(args, deps) and Domain-Driven Design

Read More →

fn(args, deps) — Bringing Order to Chaos Without Breaking Anything

18 Mar 2026

There’s a major disconnect in AI-assisted development right now. Most of the conversation assumes you’re building something new, or working from the kind of clean, stable foundation that barely exists in real engineering teams.

The reality is that most engineering teams live in legacy systems under high load, with god classes, global singletons, and console.log as observability. The kind of code where every change is a gamble.

This post shows what happens when you apply fn(args, deps) and autotel to those codebases. fn(args, deps) creates the seam for safe change; production telemetry captures the behavioural record that survives when every other spec has decayed.

John Bercow shouting "Order!" in the House of Commons

To prove the point, we’ll do this in plain JavaScript, not TypeScript.

Read More →

How fn(args, deps) supports SOLID-style design in TypeScript

18 Mar 2026

A lot of developers learn the SOLID principles through class-heavy examples.

That is probably why the conversation so often gets stuck there.

People start to associate SOLID with inheritance hierarchies, interface forests, service classes, and object-oriented ceremony.

But the useful part is not the ceremony.

It is the design pressure.

fn(args, deps) and SOLID principles

One of the simplest ways to apply that pressure in plain TypeScript is this shape:

fn(args, deps)

Where args is the input for this call and deps is the set of collaborators the function needs. You can read that as: data in, capabilities in.

fn(args, deps) is not a replacement for SOLID. It is a simple function shape that makes several SOLID ideas easier to apply without forcing you into class-heavy design.

Read More →

fn(args, deps) Is Programming to Interfaces — And That's How You Control Nondeterminism

18 Mar 2026

Many software systems fail for one very boring reason.

Not because of microservices. Not because of monoliths. Not because of whatever methodology war is trending this week.

They fail because they are unpredictable.

If you make a change and you cannot reliably determine the impact, you cannot safely evolve the system. And when you cannot evolve it, it starts behaving like legacy.

Determinism is the bridge between "works on my machine" and "works every time, everywhere."

fn(args, deps) gets you there — not because it is a clever trick, but because it makes boundaries explicit. Your logic programs to interfaces, which is what lets you control sources of nondeterminism.

Read More →

fn(args, deps) and Composition — Know When to Nest the Dolls

18 Mar 2026

Just because two pieces of code look the same does not mean they are the same.

The most common architecture mistake is not too little abstraction. It is too much, too early. You see duplication, you extract a shared module, and six months later that module is a monster held together by special cases and boolean flags.

Dan Abramov gave a talk about this called The Wet Codebase. The core argument: the wrong abstraction is far more expensive than duplication. Once an abstraction exists, it creates inertia. Nobody wants to be the person who suggests copy-paste.

fn(args, deps) changes this calculus. It makes abstractions cheap to create, cheap to test, and cheap to undo.

When a function's deps grow too large, that can be a signal that some responsibility has stabilized into its own function — and that new function itself follows fn(args, deps). (This is basically SRP pressure showing up in your signature; see the SOLID post for that framing.)

Russian dolls. Each layer independently testable. Each layer reversible.

fn(args, deps) is all you need

Read More →

fn(args, deps) and Hexagonal Architecture

17 Mar 2026

Hexagonal architecture often gets introduced with a lot of diagrams, vocabulary, and ceremony.

Ports. Adapters. Application core. Inbound and outbound boundaries.

That framing is useful, but it can also make the idea feel more abstract than it really is.

fn(args, deps) and hexagonal architecture

You may also hear this called "ports and adapters" — same idea, different name. The port is what a function needs; the adapter is what fulfils it.

A simpler way to understand it is this:

Hexagonal architecture is mostly about keeping business logic independent from delivery and infrastructure details.

One practical way to do that in plain TypeScript is this shape:

fn(args, deps);

Where args is the input for this use case, and deps is the set of external capabilities it needs. You can read that as: data in, capabilities in.

In plain TypeScript, fn(args, deps) is a simple way to implement the core idea of hexagonal architecture without turning the pattern into ceremony.

Read More →

Composition Roots and fn(args, deps)

16 Mar 2026

A lot of developers hear "dependency injection" and immediately think of containers, decorators, registration APIs, lifecycle scopes, and framework magic.

That reaction is understandable.

But that association often leads people to overcomplicate a problem that has a much simpler starting point.

Composition roots and fn(args, deps)

At its core, dependency injection just means this:

Pass collaborators in explicitly instead of reaching for them implicitly.

One of the simplest ways to do that in plain TypeScript is this shape (introduced in the series starting point: fn(args, deps)):

fn(args, deps)

Where args is call-specific input and deps is the set of collaborators the function needs.

You can read that as: data in, capabilities in.

fn(args, deps) is flexible enough to support composition, testing, and clean application wiring without forcing you into a DI framework.

Read More →

AI Makes It Easy to Build. That Doesn’t Mean We Should Ship More

12 Mar 2026

Dax is spot on.

Read More →

When it comes to using AI, be like Luke

09 Mar 2026

I love using AI coding tools. As someone who definitely gets their money’s worth, I feel more productive than ever.

But sometimes, just like Obi-Wan told Luke, you need to let go.

When it comes to using AI, be like Luke

Control the instinct to reach for AI all the time.

Trust yourself.

Read More →

Why Matt Pocock Is Right About Making Codebases AI Agents Love

05 Mar 2026

Matt Pocock is once again on the money in his video “How To Make Codebases AI Agents Love”.

Here's why I agree with him.

Read More →

If You Only Enforce One Rule for AI Code, Make It fn(args, deps)

04 Mar 2026

AI coding agents produce code faster than you can review and understand it.

One pattern works in both new and legacy codebases because you can adopt it incrementally, without breaking callers.

fn(args, deps) is all you need

For business logic, treat every function as having two inputs: data (args) and capabilities (deps).

Without a clear constraint, generated code becomes harder to reason about: dependencies disappear, side effects spread, composition gets messy. This is why visible structure is essential.

fn(args, deps) is that constraint

For existing code, start with:

functionName(args, deps = defaultDeps)

Dependencies are explicit, not hidden.

There’s no framework and no package to install.

Read More →

Visualising Awaitly Workflows with awaitly-visualizer

07 Feb 2026

You have an Awaitly workflow: a few steps, some dependencies, typed results. It works. When someone asks "what does this do?" or you need to debug a run, you're left tracing through code.

What if you could see the same workflow as a diagram? awaitly-visualizer plugs into your workflow's events and turns them into that picture. For a checkout that runs fetchCart, validateCart, processPayment, then completeOrder, you get output like this:

┌── checkout ────┐
│  ✓ fetchCart   │
│  ✓ validateCart│
│  ✓ processPayment
│  ✓ completeOrder
│  Completed     │
└────────────────┘

Same idea as Mermaid flowcharts: steps, order, success and failure. This post walks through adding it step by step. All of the code below lives in a test in the repo so you can run it yourself.

Read More →

No, the sun does not shine out of Claude’s backside

30 Jan 2026

As of today, Opus 4.5 is the best coding model I've used. That is not praise by vibes. That is, after building libraries and utilities that fixed problems I could not solve with the tools I was using before.

The progress is impressive.

However, it’s not all sunshine and rainbows, as people on social media and YouTube claim.

Cartoon illustration of a pig driving and a chicken riding along, suggesting responsibility versus involvement.

Read More →

Stop Throwing Errors. Awaitly Makes Async Error Handling Actually Work

22 Jan 2026

Stop Throwing Errors

We've all written this code:

const lambdaHandler = async () => {
  try {
    const db = await connectToDb();
    const result = await errorHandler({ taskId, error }, { db });
    return { statusCode: 200, body: { message: 'Success', task: result } };
  } catch (error) {
    return { statusCode: 500, body: { message: 'Error' } };
  }
}

That catch (error) swallows everything. Was it a "task not found"? A database connection failure? A permissions issue? Who knows.

Throwing exceptions for expected failures is like using GOTO. You lose the thread.

Awaitly fixes this by treating errors as data, not explosions. This guide teaches the patterns one concept at a time.

Read More →

Instrumenting Message Queues? Autotel Handles the Ceremony

18 Jan 2026

The OneUptime team is spot on in their Instrument Message Queues with OpenTelemetry post.

Inject trace context on the producer, extract on the consumer; use PRODUCER and CONSUMER span kinds; set semantic conventions (messaging.system, messaging.destination.name, messaging.operation, Kafka partition/offset/consumer group).

They show the raw OpenTelemetry code. It's comprehensive. It's also verbose. Every team ends up re-implementing the same patterns: inject, extract, span kinds, semantic attributes, error handling.

We've all been there: copying "best practice" code from blog posts and adapting it for our broker.

Their key insight:

For batch processing, use a batch span with links or child spans to contributing traces.

But there's still a gap...

Read More →

Message Isolation? Autotel Makes Tenant Context Flow

17 Jan 2026

The Signadot team is spot on in their Testing Event-Driven Architectures with OpenTelemetry post.

Message isolation using a shared queue: propagate tenant ID in Kafka message headers; consumers use tenant ID for selective message consumption.

They make the case that infrastructure duplication is expensive. Instead of separate Kafka clusters per environment, use tenant ID filtering on a shared queue. Instrument producers and consumers for context propagation.

We've all been there: maintaining four "identical" Kafka setups that slowly drift apart.

Their key insight:

Requires modifying consumers and using OpenTelemetry for context propagation.

But there's still a gap...

Read More →

Request-Level Isolation? Autotel Propagates Context Automatically

16 Jan 2026

The CNCF team is spot on in their Testing Asynchronous Workflows using OpenTelemetry and Istio post.

Request-level isolation is the most cost-effective approach.

They make the case against duplicating infrastructure for testing. Instead of spinning up separate Kafka clusters per tenant, use OpenTelemetry Baggage to propagate tenant ID through async flows. Consumers filter by tenant ID. Istio handles routing.

We've all been there: every team has their own "staging Kafka" and costs balloon.

Their key insight:

Use OpenTelemetry Baggage to propagate tenant ID through sync and async. When publishing to Kafka, producers inject trace context (including baggage) into message headers; consumers extract and make routing decisions.

But there's still a gap...

Read More →

End-to-End Tracing? Autotel Makes It Automatic

15 Jan 2026

The OSO team is spot on in their End-to-End Tracing in Event Driven Architectures post.

Traces break at queues unless you extract context from message headers and put it in the appropriate context.

They walk through the real pain: stateful processing loses trace context in caches, Kafka Connect can only do batch-level tracing, and every team ends up writing custom interceptors and state store wrappers.

We've all been there.

Their key insight:

In Kafka Streams and Kafka Connect this often means manual work: interceptors, state stores, batch spans, or extending tracing logic to extract from headers.

But there's still a gap...

Read More →

Logging Sucks. Autotel Makes Wide Events the Default

08 Jan 2026

Boris is spot on in his Logging Sucks post

logs are optimised for writing, not querying

He explains why debugging in production feels like archaeology.

You grep for user-123, find it logged 47 different ways, then spend an hour correlating timestamps across services.

We've all been there.

His wide event example nails it:

{
  "user": {"id": "user_456", "subscription": "premium", "lifetime_value_cents": 284700},
  "cart": {"item_count": 3, "total_cents": 15999, "coupon_applied": "SAVE20"},
  "payment": {"method": "card", "provider": "stripe", "latency_ms": 1089},
  "error": {"type": "PaymentError", "code": "card_declined", "stripe_decline_code": "insufficient_funds"}
}

One event. High-cardinality keys (user.id, traceId). High dimensionality. Queryable.

But there’s still a gap…

Read More →

Reranking: Improving Search Relevance with the AI SDK

23 Dec 2025

Reranking improves search relevance by reordering documents based on their relevance to a query. Unlike embedding-based similarity search, reranking models are specifically trained to understand the relationship between queries and documents, often producing more accurate relevance scores.

Reranking

Read More →

AI Code Needs Rules, Not Rituals (The Proof)

18 Dec 2025

I wrote previously about why AI-generated code needs rules, not rituals.

AI coding agents aren't going anywhere. They're excellent at exploring ideas, generating boilerplate, and moving fast. But speed without reliability just ships bugs faster. And without constraints, AI-generated code is unreliable by default.

Prompting is a ritual. Linting is a rule.

Here is the proof.

Agent

Read More →

AI Code Needs Rules, Not Rituals

20 Oct 2025

Coding agents are here to stay, and I know I’m absolutely right about that. While we're all getting used to workflows using AI-powered coding agents, we now live in the world of dark arts and rituals.

We spend hours tweaking prompts, creating elaborate Claude.md, Agents.md and other files formatted in a particular way, stored in a particular way essentially performing black magic to hope our LLM agent adheres to our team's best practices and coding patterns.

On a good day, it works. On others, the behaviour is random.

Here's the problem: Random is not good enough for production.

We're trying to force a non-deterministic, generative tool to be a deterministic rule-follower. This is the wrong approach.

Instead, we should let AI do what it does best: be creative and generative, helping us achieve and realise our desired outcomes while following instructions and examples for how, what, and where it should generate.

My advice? Stop relying on hope-driven prompting. Start using linters to guarantee your standards.

Agent

Read More →

Agent: The Sugar Syntax of streamText

19 Sep 2025

For most of us, AI still feels like a black box. We send it a prompt and we get back a blob of text. Maybe we write some code to call a tool; maybe we juggle a few callbacks. We tell ourselves that this is just how things work: a model can only generate tokens, and tools can only run in our code.

But what if this mental model is the problem?

In this post I want to argue that the Agent pattern in the AI SDK is as revolutionary for AI development as useState and useEffect were for React. Just like React's client/server directives annotate where code runs across the network, the Agent API annotates where logic runs across the AI/model boundary.

Agent

Read More →

Build Safer, Faster, and More Reliable AI Apps with AI SDK Middleware

27 Aug 2025

Having recently built an AI Guardrails library for the AI SDK, I wanted to share what I learned along the way. This post will walk you through how you can write your own middleware, and why it's such a game-changer for building robust AI applications.

Design AI features that are safer, faster, and easier to evolve by layering language model middleware. This guide explains how to use AI SDK middleware to transform inputs, post-process outputs, enforce safety rules, cache results, observe performance, and handle streaming using a clean, composable approach aligned with official guidance.

Read More →

Building Reliable AI Agents Series: Factor 2 – Own Your Prompts

28 Jul 2025

This is the second post in a series of posts about building reliable AI agents.

When building AI agents, where do your prompts live? If they're hidden inside frameworks or scattered across configuration files, you're missing a fundamental principle of maintainable AI systems: treating prompts as first-class code citizens.

Read More →

Hannah Hampton. A proper comeback story.

27 Jul 2025
Hannah Hampton
Photo by Katie Chan (KTC), licensed under CC BY-SA 4.0.

⚽️ Dropped by then and current manager Sarina Wiegman from the England squad in 2022 amid concerns over her attitude and conduct, she remained outside the national setup for almost two years.

⚽️ Born with strabismus and challenged by depth‑perception issues, she was told by doctors she should not play football.

⚽️ Rebuilt her career at Chelsea, playing an integral role in winning the treble last season.

⚽️ Chosen as England's first-choice goalkeeper ahead of the great and brilliant Mary Earps.

⚽️ Delivered under pressure in the quarter‑final versus Sweden, even with a bloody nose. Named Player of the Match.

Tonight, she starts in a European final for England.

Even when things don’t go your way, never give up.

Read More →

Building Reliable AI Agents Series: Factor 1 – Natural Language → Tool Calls

14 Jul 2025

This is the first post in a series of posts about building reliable AI agents.

Unlock reliable, testable AI agents by treating your LLM as a parser, not an executor. Learn how converting natural language into structured tool calls leads to predictable, scalable systems.

Read More →

Building Reliable AI Agents Series: A 12-Factor Methodology

14 Jul 2025

After building production AI systems over the past few years, thanks to HumanLayer, I’ve learned that most agent failures aren’t about the LLM, they’re about architecture.

That’s why I’m creating a series of posts sharing the 12-Factor Agents methodology using Mastra.

In each part, I’ll break down one principle that transforms fragile prototypes into robust, production-ready AI agents.

Read More →

Demystifying MCP: Coaching Colleagues Through the New AI Protocol

30 Jun 2025

Last month, I had the fantastic opportunity to run a hands-on Model Context Protocol (MCP) workshop. It was a chance to mentor and coach colleagues, from engineers and testers to product owners, as we explored what MCP is, how it works, and why it is generating so much momentum across AI and developer communities.

My goal was not just to share knowledge. It was to guide the learning journey and build a shared, foundational understanding of this powerful emerging standard.

Workshop Highlights

We covered everything from core protocol basics and prompt crafting to real world patterns such as:

Read More →

Understanding Commands & Events with Orchestration & Choreography

18 Jun 2025

Designing distributed systems has never been more challenging. As teams embrace microservices and event‑driven architectures, a persistent myth has arisen: commands equal orchestration, and events equal choreography. This tidy equivalence often becomes a mental shortcut but conceals the deeper truth of control‑flow patterns versus messaging semantics. As many practitioners have observed, collapsing these separate dimensions can restrict your system's flexibility and resilience.

Person thinking about the difference between commands and events.

In this article, we'll demystify these concepts and show how to apply them independently. You'll discover how separating semantics (commands vs. events) from control flow (orchestration vs. choreography) grants you greater architectural freedom and clearer, more maintainable workflows.

Read More →

From Gatekeeper to Enabler: Rethinking Engineering in the Age of the Citizen Developer

09 Jun 2025

Last month, our company hackathon became a vivid illustration of a broader shift in how software gets built. Across teams, from sales to support, product to engineering, domain experts huddled around laptops, experimenting with AI powered platforms such as V0, Lovable and Bolt. Within hours, they'd fashioned interactive prototypes complete with navigation flows and validation rules, all without writing a single line of traditional code.

Although these early demos relied on mock data, the fact that non-engineers could conjure usable software unaided was striking.

As a full-stack engineer accustomed to crafting CRUD apps from the ground up, I found myself asking a new question: How might we empower these citizen developers to build more often, more securely, and with live data? Their deep problem domain knowledge meant they moved swiftly, iterated boldly and learned faster than any handoff-laden process could permit.

An isometric illustration of a futuristic highway under construction, where software engineers in hard hats are laying down glowing code-shaped road segments. An isometric illustration of a futuristic highway under construction, where software engineers in hard hats are laying down glowing code-shaped road segments.

Over my two decades in software, I've discovered my highest leverage isn't in writing every screen or endpoint myself, but in architecting robust APIs, infrastructure and tooling so that others can deliver user value. In this two-part series, I'll share how engineers can transition from gatekeeping code to enabling creation at scale. In this first instalment, we'll explore the mindset shifts and guiding principles. Part 2 will dive into concrete patterns and architectural strategies for secure, sustainable enablement.

Read More →

Dual-Layer Locking: Redis for Speed, PostgreSQL for Reliability

02 Jun 2025

Two users withdraw money from the same account at exactly the same moment. Your system processes both requests, your database balance goes negative, and you wake up to an incident report. Sound familiar?

What if you could combine Redis's millisecond response times with PostgreSQL's bulletproof consistency? This dual-layer locking pattern does exactly that, giving you both speed and safety.

Redis and PostgreSQL logos side by side.

In this post, we'll explore a pattern that combines Redis's speed with PostgreSQL's reliability to prevent race conditions at scale.

Read More →

Impact Through Empowerment: Contributing Without Direct Participation

24 Mar 2025

In my career, I have realised that the best companies celebrate each success in a genuine, informal way. They avoid contrived rituals that can feel like an artificial façade.

In these organisations, individuals willingly help colleagues reach shared goals.

Take Janet from my local Parkrun, for example.

Janet at my local Parkrun encouraging each participant to achieve their goals

Although she does not compete, she choose to find the time to cheer on each participant to achieve their goals... even on cold Saturday mornings.

Read More →

From Static to Stateful: Revolutionising AI Communication with the MCP Protocol

10 Mar 2025

MCP standardises how AI applications communicate with external systems using JSON‑RPC 2.0 over stateful, bidirectional connections. Unlike standard JSON‑RPC, which treats every request independently, MCP augments the protocol by embedding session tokens and context IDs into each message.

This enhancement provides state, enabling advanced, multi‑step interactions and dynamic module loading at runtime. Such a stateful design is essential for modern, agile AI systems.

Imagine an AI‑powered payments system that dynamically integrates a new fraud detection module during operation. MCP allows the system to load this module on the fly without a full redeployment, provided the server supports dynamic module loading.

In this post, we'll explore the MCP protocol, its core components, and how it compares to traditional REST and GraphQL APIs.

Read More →

Multi-Agent Patterns: A Practical Guide

05 Mar 2025

multi-agent-patterns.jpg

AI applications are shifting from monolithic large language models to modular, multi-agent systems—a transformation that enhances performance, flexibility, and maintainability.

In my talk about AI Agents last September, I said AI agents would become increasingly popular. Today, we see this shift happening across industries. By breaking down complex tasks into specialised components, engineers can design smarter, more scalable AI workflows.

Analogy: Think of multi-agent systems like a well-coordinated orchestra. Each musician (agent) has a specific role, and together, they create a harmonious performance. In software, this means dividing complex problems into manageable, specialised parts that work in concert.

In this guide, we'll explore four key multi-agent patterns, using travel booking as an example. You'll learn how to choose the right pattern for your application, implementation strategies, and error-handling techniques to build robust multi-agent AI systems.

Read More →

When to Use Agentic Systems

02 Mar 2025

Agentic systems, where multiple AI agents collaborate through decision-making and handoffs, shine in specific scenarios but add operational complexity.

In this post, we'll explore the scenarios where agentic systems are most effective and the challenges you may face when using them.

Read More →

Next.js after: Track Analytics and Events Without Slowing Down Your App

27 Feb 2025

Imagine: you run a busy online shop where every moment matters.

Your customers expect the fastest page loads, yet you still require dependable analytics to understand user behaviour and drive informed business decisions.

Relying solely on client-side tracking with tools like PostHog can be fraught with challenges. Many users have ad blockers or privacy extensions that prevent tracking scripts from running, and even when these scripts do execute, they can slow down page rendering and affect conversion rates.

In this post I'll go over how you can use Next.js 15’s next/after API to handle analytics and events without slowing down your site.

Read More →

Event-Driven Architecture: How I Name Events (A Practical Guide With Examples)

24 Feb 2025

Getting event names right in event-driven architecture and Domain-Driven Design (DDD) is essential for clarity, consistency, and scalability. A key decision is using singular or plural terms in event names.

Here's how I approach it, with examples and reasoning to help you make the best choice.

Read More →

Building TypeScript Libraries' A Guide to Explicit Exports, Tree Shaking, and Common Pitfalls

20 Feb 2025

builder building typescript sign

Building a TypeScript library that is both maintainable and optimised for modern bundlers requires careful consideration of exporting functions, types, and other constructs from your modules.

With several strategies available, from wildcard re-exports to namespaced exports, explicitly named re-exports have emerged as the clear winner. This post explores the alternatives with examples based on an order management system.

We'll discuss the benefits of exporting types, how to organise multiple entry routes (such as in /src/orders and /src/users), and review the necessary TypeScript configuration options (such as verbatimModuleSyntax) and package.json tweaks. Additionally, we'll explain how this approach helps prevent circular dependency challenges by enforcing a clear dependency graph.

Read More →

Choosing Your React Framework in 2025: Understanding Next.js Trade-offs

13 Feb 2025

I love Next.js. It's been my framework of choice since my first tiny contribution in 2017.

Yesterday, I joined a team discussion about which React framework to choose. In 2025, that decision is harder than ever, with so many excellent alternatives available.

While Next.js is recommended by React themselves, and is a very popular choice, the not-so-good parts are often left out on platforms like LinkedIn and YouTube.

I plan to write about the good things about Next.js 15 in the future, but here are some challenges I still face using it today.

Read More →

Tracking Every Decision in Payments: The Context Pattern Approach

31 Jan 2025 Designing a payment system is like any other software solution I've worked on. At first, it appears straightforward, but real-world factors like currency conversions, fees, payee validations and external APIs quickly add complexity. Read More →

Chasing the AI God: Why We Worship Every New Model

29 Jan 2025

It's like a new diet pill has hit the market, promising instant weight loss with no effort, and now everyone's scrambling to get their hands on it because it's all over their TikTok.

Some folks are talking about DeepSeek as if it's the second coming of AI.

The clamour might be because it's the first serious open non-US model with reasoning capabilities from China.

Chasing the AI God

Most people are confused about which version of DeepSeek they're using.

Most providers offer a distilled, watered-down version that'll run anywhere, so you're not getting the actual full fat version people are talking as to really see its magic, you'd need a small fortune in computing power.

It's like being promised a rare vintage white wine, only to find the bottle filled with slightly grape-scented water. Same brand, same label, but all the depth and character stripped away, leaving you with a hollow imitation.

The hype suggests it can keep pace with anything OpenAI does, which might sound thrilling if you're already tired of whatever ChatGPT or its siblings spit out.

But here's where the alarm bells should start ringing.

Read More →

Lessons from Building AI Products with Stakeholders

27 Jan 2025

Building AI products with stakeholders requires a fundamentally different approach than traditional software development.

From my experience working with AI, success depends not only on technical implementation but also on bridging the gap between AI capabilities and stakeholder expectations.

In this post, I’ll share lessons from guiding stakeholders through AI’s possibilities and limitations.

Read More →

Whose Responsibility Is It Anyway? Defining Boundaries with Event-Driven Architecture

20 Jan 2025

Defining clear boundaries is essential to building clean, scalable, and reliable architectures.

In my experience, organisational demands often override the focus on boundaries and domain-driven design. This reflects the tension between following technical best practices and delivering business outcomes quickly. While theoretical approaches are widely discussed at conferences, in books, and in videos, the practical implementation of these ideas is often shaped by cultural dynamics, resource constraints, tight deadlines, and internal politics.

In this post, we'll explore the challenges of maintaining boundaries and potential solutions, using a payment system as an example. This system facilitates transactions between clients and payment providers, such as PayPal or Stripe, highlighting the distribution of responsibilities within its architecture.

Read More →

The Determinism Dilemma: Why Traditional Software Engineers Struggle with AI Development

13 Jan 2025

The goal of creating something "predictable," reliable, and consistent is a shared principle across all the teams I've worked with throughout my career.

Knowing that the same code would always return the same output when given the same inputs was the foundation of everything we built.

We aimed for no surprises, no matter how complex a workflow might be. Whether implicitly or explicitly using finite state machines, this determinism enabled us to build testable, monitorable, maintainable, and, most importantly, predictable workflows.

We read and shared ideas at conferences, promoting patterns and principles like SOLID and DRY to create functional, composable, and extensible software.

Read More →