Temporal Has a Free Durable Workflow Engine That Never Loses Your State

What happens when your payment processing crashes mid-transaction? With regular code — data loss. With Temporal — it resumes exactly where it left off. Temporal makes your workflows durable, reliable, and resumable by default.

What Temporal Gives You for Free

  • Durable execution — workflows survive crashes, restarts, and deployments
  • Automatic retries — configurable retry policies per activity
  • Long-running workflows — days, weeks, months
  • Timers — sleep for hours/days without holding resources
  • Versioning — update workflow code without breaking running instances
  • Visibility — web UI showing all workflow states
  • TypeScript SDK — fully typed, great DX
  • Self-hosted — Docker Compose or Kubernetes

Quick Start

# Run Temporal server
docker compose up -d

# Create project
npx @temporalio/create@latest my-app
cd my-app
npm run start

Define a Workflow

// workflows.ts
import { proxyActivities, sleep } from '@temporalio/workflow';
import type * as activities from './activities';

const { sendEmail, chargePayment, provisionAccount } = proxyActivities<typeof activities>({
  startToCloseTimeout: '30 seconds',
  retry: { maximumAttempts: 3 }
});

export async function onboardUser(email: string, plan: string): Promise<void> {
  // Step 1: Send welcome email
  await sendEmail(email, 'Welcome!');

  // Step 2: Charge payment
  const paymentId = await chargePayment(email, plan);

  // Step 3: Provision account
  await provisionAccount(email, plan);

  // Step 4: Wait 3 days, then send tips
  await sleep('3 days'); // Durable! Survives server restarts.
  await sendEmail(email, 'Here are some tips...');

  // Step 5: Wait 7 days, check engagement
  await sleep('7 days');
  await sendEmail(email, 'How's it going?');
}

If the server crashes after Step 2, Temporal resumes at Step 3 — not from the beginning.

Activities (Side Effects)

// activities.ts
export async function sendEmail(to: string, subject: string): Promise<void> {
  await resend.emails.send({ to, subject, from: '[email protected]' });
}

export async function chargePayment(email: string, plan: string): Promise<string> {
  const session = await stripe.checkout.sessions.create({
    customer_email: email,
    line_items: [{ price: plans[plan], quantity: 1 }]
  });
  return session.id;
}

export async function provisionAccount(email: string, plan: string): Promise<void> {
  await db.accounts.create({ data: { email, plan, status: 'active' } });
}

Start a Workflow

import { Client } from '@temporalio/client';

const client = new Client();

const handle = await client.workflow.start('onboardUser', {
  taskQueue: 'main',
  workflowId: `onboard-${userId}`,
  args: ['[email protected]', 'pro']
});

// Check status
const result = await handle.result();

Use Cases

  • Payment processing — charge, fulfill, refund with guaranteed completion
  • User onboarding — multi-day sequences that never lose state
  • Data pipelines — ETL that resumes from where it failed
  • Order fulfillment — payment → inventory → shipping → notification
  • Subscription management — billing cycles, upgrades, cancellations

Temporal vs BullMQ vs Inngest vs Trigger.dev

Feature Temporal BullMQ Inngest Trigger.dev
Durable execution Core feature No Yes Yes
Long-running Days/months Hours Limited Hours
Workflow state Automatic Manual Automatic Manual
Versioning Built-in None None None
Visibility UI Full Bull Board Dashboard Dashboard
Self-hosted Yes Yes (Redis) No No
Complexity Medium Low Low Low

The Verdict

Temporal is for workflows that MUST complete — payment processing, order fulfillment, data pipelines. If a crash or restart can lose your state, Temporal prevents it. Enterprise-grade reliability for any application.

Need help building production web scrapers or data pipelines? I build custom solutions. Reach out: [email protected]

Check out my awesome-web-scraping collection — 400+ tools for extracting web data.

Weekly Challenge #5 : The CSS‑Only Secret Menu 🔐

Today is the 30th, and we’re back with Weekly Challenge #5.

This one’s sneaky, weird, and honestly super satisfying when you get it right.

You’re building a CSS‑only secret menu — a hidden little UI easter egg that only appears when the user does something specific (and not obvious at all).

Quick shoutout to @francistrdev for last week’s challenge — it was genuinely good, and yes, it’s in progress. I didn’t forget, I’m just slow and easily distracted by shiny CSS things.

The Mission

Make a menu that starts completely invisible and only shows up when the user triggers it in a clever way.

Not a normal button.

Not a big “open menu” link.

Something subtle. Something sneaky. Something that makes people go “wait… how did I open that?”

The Rules

  • No JavaScript
  • Menu must start fully hidden
  • User must perform a non‑obvious action to reveal it
  • Menu must have at least 5 items
  • The reveal must be animated

The Goal

Make it feel like a secret dev panel.

A hidden door.

A little “you found the easter egg” moment.

Bonus (if you wanna go wild)

  • Add a second hidden layer
  • Add a CSS‑only lock/unlock toggle
  • Give it a theme (spy, hacker, neon, retro terminal, etc.)

How to Enter

Just drop your CodePen or GitHub link in the comments.
That’s literally it.
No deadlines, no pressure — just build something cool and share it.

Alright, that’s the challenge

Go make something sneaky.
Make it weird. Make it fun. Make it yours.

And again — thanks @francistrdev for last week’s challenge.
It’s cooking.

Reading Data from Weather Sensors with Python

The interfaces and implementations to mock the access to the WeatherHAT hardware

Apart from building a weather station, another thing I had been wanting to do for a long time was to learn Python. With this Weather Station project I satisfied both 🙂

Check the source code of this project in Codeberg.

Learning the Pimoroni’s WeatherHAT library

The first thing I did, after having everything installed, was to learn how the Pimoroni’s WeatherHAT library works in order to:

  • Read the weather data from the sensors
  • Display the weather data in the WeatherHAT’s screen

So I opened an SSH connection and started playing!

Apart from being my first steps with Python what I learned about the library was:

  • It is necessary to call the “update” method of the sensor before getting the weather readings
  • The first reading is “crap”, it is necessary to perform an initial update and wait a few seconds before getting good reading values.
  • There are two attributes for temperature: device_temperature and temperature
  • The temperature attribute is calculated by applying a default offset of 7.5ºC to the device_temperature reading.

Calibrating the Temperature

The first thing I focused on was to properly calibrate the temperature. For this I spent some days gathering temperature data from the sensor and from another nearby weather station at the same time, taking readings every hour.

What I noticed was that the difference between both sensors wasn’t a fixed value. The readings from the WeatherHAT’s sensor were always between 12ºC and 16ºC higher than those from the nearby station.

So instead of applying a fixed offset I tried a linear regression. I created a class to calculate the offset based on a given device_temperature value.

In the future I could find a better way to calculate the offset. That is why I created a TemperatureOffsetInterface.

class LinearRegressionOffset(TemperatureOffsetInterface):
    def __init__(self, config):
        self.a = 0
        self.b = 0
        self.config = config
        self.doLinearRegression()        

    def getOffset(self, x):
        offset = (self.b * x) + self.a        
        return offset

    def doLinearRegression(self):
        x = self.config['data']['x'].split(",")
        y = self.config['data']['y'].split(",")        

        x = list(map(float, x))
        y = list(map(float, y))       

        medianX = statistics.median(x)
        medianY = statistics.median(y)
        numSum = 0
        denSum = 0

        for index, xValue in enumerate(x):
            numSum = numSum + (xValue - medianX) * (y[index] - medianY)
            denSum = denSum + ((xValue - medianX) * (xValue - medianX))

        self.b = numSum / denSum
        self.a = medianY - (self.b * medianX)

I could have used a Python library to perform the linear regression but, remember, the goal of this project was to learn Python ;-).

Decoupling From The Hardware

Having solved the calibration of the temperature sensor it was time to start coding the reading system.

The process should run periodically, for example every 5 minutes. On each iteration the system should read the data from the sensor apply the temperature offset, display the data on the screen and send it to an external service.

I work on my own PC, as it is more comfortable than coding directly in the Pi. That is why I created some mocks to simulate access to the WeatherHAT hardware.

The interfaces and implementations to mock the access to the WeatherHAT hardware

This way I could code the main algorithm decoupled from the hardware.

sensor = factory.createSensor()
display = factory.createDisplay()

Once the main algorithm was done I simply had to code the hardware-specific parts. I had to upload each change to the Pi but using the examples provided by Pimoroni it was more or less easy to get the data from the sensor and show it in the WeatherHAT screen.

Sending The Data To An External Service

I wanted to see the current temperature from anywhere and also store the readings in order to generate statistic from the weather data.

So the next step was to send the data to an external service. As I hadn’t yet implemented my Weather Data Webapp, I started sending the data to Adafruit. I created a dashboard to see current data and also a graph to easily compare the temperature readings with the nearby station. This way I could check the quality of the linear regression.

I created an ConnectorInterface to first create the Adafruit connector and later the implementation to send the weather data to my Weather Data Webapp.

class ConnectorInterface:
    def send(self, WeatherData):
        pass

I also created a MockConnector to avoid sending bad data while working on the main flow of the algorithm.

Implementing An Abstract Factory

Everything was working fine, but changing which implementations to use while testing the system locally (mock) and in “production” (on the Pi) was a bit messy.

To simplify the mode selection I applied the Abstract Factory Pattern. This way, with a single configuration setting, I could load the right set of components (Sensor, Display and Connector).

class SystemFactoryInterface:

    def createDisplay(self) -> DisplayInterface:
        pass

    def createConnector(self, config) -> ConnectorInterface:
        pass

    def createSensor(self, config) -> SensorInterface:
        pass     

Conclusion

I enjoyed this part a lot. Learn Python, read the data from the sensor, calibrate the temperature, apply some design patterns, … It brought back the feeling I had when I first started learning to program.

Maybe because it is a project that is really different to the ones I more used to develop, maybe because It mixes different thing or maybe both :-).

In the next article I’ll try to explain the implementation of the Weather Data Webapp. This time with PHP and Symfony, a language and a framework that I’m more used to work with.

Understanding Data Modelling in Power BI: Joins, Relationships, and Schemas Explained

Introduction

Working with raw data often introduces challenges such as duplication, inconsistency, and difficulty in extracting meaningful insights.

Data modelling in Power BI addresses these challenges by structuring data into related tables, enabling efficient querying, accurate calculations, and scalable reporting.

This article explores:

  • SQL joins and their behaviour
  • Power BI relationships and how they differ from joins
  • Data modelling schemas
  • Practical implementation in Power BI

We’ll use a simple school dataset to ground these concepts.

1. Sample Dataset

We will work with three core tables:

Students (Dimension Table)

StudentID Name Age Class
101 Alice 14 Form 2
102 Brian 15 Form 3
103 Catherine 14 Form 2
104 David 16 Form 4

Test Scores (Fact Table)

ScoreID StudentID Subject Score
1 101 Math 85
2 101 English 78
3 102 Math 92
4 105 Science 88

⚠️ Note: Student 105 does not exist in the Students table—this represents a referential integrity issue.

📊 View Dataset Online

Explore the dataset directly: 👉here!

Teachers (Dimension Table)

TeacherID Name Subject
T1 Mr. Kim Math
T2 Ms. Asha English
T3 Mr. Otieno Science

Data Roles

  • Fact Table: Test Scores (transactional/measurable data)
  • Dimension Tables: Students, Teachers (descriptive attributes)

2. What is Data Modelling?

Data modelling is the process of:

Organizing data into structured tables and defining relationships that enable efficient querying and accurate analysis.

In analytical systems like Power BI, this typically involves:

  • Separating facts from dimensions
  • Establishing keys (e.g., StudentID)
  • Designing a schema optimized for performance

3. Joins vs Relationships

Joins (Power Query Layer)

A join is a data transformation operation that combines rows from two tables based on a matching condition.

Key Characteristics:

  • Produces a new table
  • Data is physically merged
  • Can introduce duplication

Example:

Joining Students and Test Scores results in repeated student records for each score.

Relationships (Model Layer)

A relationship is a logical link between tables based on matching columns.

Key Characteristics:

  • Tables remain separate
  • Data is combined at query time
  • Enables efficient filtering and aggregation via DAX

Critical Difference

Aspect Join Relationship
Type Physical transformation Logical connection
Location Power Query Model View
Data duplication Yes No
Performance impact Higher memory usage Optimized

4. SQL Joins Explained (with Examples)

Joins determine how rows from two tables are combined based on a matching condition.

We will use:

  • Students table (left table)
  • Test Scores table (right table)

INNER JOIN

Definition

Returns only rows where a match exists in both tables.

SQL Example

SELECT s.StudentID, s.Name, t.Subject, t.Score
FROM Students s
INNER JOIN TestScores t
ON s.StudentID = t.StudentID;

Result

StudentID Name Subject Score
101 Alice Math 85
101 Alice English 78
102 Brian Math 92

Explanation

  • Student 103 and 104 are excluded (no scores)
  • Score with StudentID 105 is excluded (no matching student)

LEFT JOIN

Definition

Returns all rows from the left table, with matching rows from the right.

SQL Example

SELECT s.StudentID, s.Name, t.Subject, t.Score
FROM Students s
LEFT JOIN TestScores t
ON s.StudentID = t.StudentID;

Result

StudentID Name Subject Score
101 Alice Math 85
101 Alice English 78
102 Brian Math 92
103 Catherine NULL NULL
104 David NULL NULL

Explanation

  • All students are included
  • Missing scores appear as NULL
  • Useful for identifying missing activity

RIGHT JOIN

Definition

Returns all rows from the right table and matching rows from the left.

SQL Example

SELECT s.StudentID, s.Name, t.Subject, t.Score
FROM Students s
RIGHT JOIN TestScores t
ON s.StudentID = t.StudentID;

Result

StudentID Name Subject Score
101 Alice Math 85
101 Alice English 78
102 Brian Math 92
NULL NULL Science 88

Explanation

  • Score for StudentID 105 appears
  • Missing student info is NULL
  • Highlights data inconsistencies

FULL OUTER JOIN

Definition

Returns all rows from both tables, matched where possible.

SQL Example

SELECT s.StudentID, s.Name, t.Subject, t.Score
FROM Students s
FULL OUTER JOIN TestScores t
ON s.StudentID = t.StudentID;

Result

StudentID Name Subject Score
101 Alice Math 85
101 Alice English 78
102 Brian Math 92
103 Catherine NULL NULL
104 David NULL NULL
NULL NULL Science 88

Explanation

  • Combines LEFT and RIGHT JOIN behavior
  • Shows all matched and unmatched records

LEFT ANTI JOIN

Definition

Returns rows from the left table with no match in the right.

SQL Equivalent

SELECT s.StudentID, s.Name
FROM Students s
LEFT JOIN TestScores t
ON s.StudentID = t.StudentID
WHERE t.StudentID IS NULL;

Result

StudentID Name
103 Catherine
104 David

Explanation

  • These students have no test scores
  • Useful for identifying inactivity

RIGHT ANTI JOIN

Definition

Returns rows from the right table with no match in the left.

SQL Equivalent

SELECT t.StudentID, t.Subject
FROM TestScores t
LEFT JOIN Students s
ON s.StudentID = t.StudentID
WHERE s.StudentID IS NULL;

Result

StudentID Subject
105 Science

Explanation

  • This score has no valid student
  • Indicates a data quality issue

Key Insight

Each join answers a specific business question:

Join Type Business Question Example from Our Dataset
INNER What data exists in both tables? Which students have recorded test scores? → Alice and Brian
LEFT What records are missing related data? Which students are in the system but have not taken any tests? → Catherine and David
RIGHT What data exists on the secondary side? Are there any test scores without valid student records? → Score for StudentID 105
FULL What does the complete dataset look like? What is the full picture of students and scores, including missing or unmatched data?
LEFT ANTI What records in the main table have no matches? Which students have no test scores at all? → Catherine and David
RIGHT ANTI What records are invalid or inconsistent? Which test scores do not belong to any student? → StudentID 105

5. Power BI Relationships

Cardinality

Defines how rows relate between tables.

Type Description
1:M One dimension record relates to many fact records
M:M Multiple matches on both sides
1:1 Unique mapping

👉 Best practice:
Use 1:M from Dimension → Fact

Cross-Filter Direction

Defines how filters propagate between tables.

Type Behaviour
Single One-way filtering
Both Bi-directional filtering

⚠️ Overusing bi-directional filtering can introduce ambiguity.

Active vs Inactive Relationships

  • Only one relationship can be active between two tables
  • Inactive relationships can be used via DAX (USERELATIONSHIP)

6. Fact vs Dimension Tables

Fact Table

  • Contains measurable data
  • Often large
  • Example: Test Scores

Dimension Table

  • Contains descriptive attributes
  • Used for filtering and grouping
  • Example: Students

7. Schemas

Star Schema

          Students
             |
Teachers — Test Scores — Subjects

Characteristics:

  • Central fact table
  • Denormalized dimensions
  • Optimized for performance

Snowflake Schema

Students
   |
Address
   |
Region
   |
Test Scores

Characteristics:

  • Normalized dimensions
  • Reduced redundancy
  • Increased complexity

Flat Table (Denormalized Model)

All data stored in one table.

Trade-offs:

  • Simpler structure
  • Poor scalability
  • High duplication

8. Implementation in Power BI

Power Query (Data Preparation)

  • Clean data
  • Perform joins
  • Handle missing values

Model View (Data Modelling)

  • Create relationships
  • Define cardinality
  • Configure filters

Best Practice Workflow

  1. Load data
  2. Clean in Power Query
  3. Avoid unnecessary joins
  4. Build relationships in Model View
  5. Validate using visuals

9. Common Modelling Issues

  • Data duplication from excessive joins
  • Incorrect cardinality
  • Ambiguous filter paths
  • Missing or inconsistent keys

Conclusion and Key Takeaways

Data modelling is the foundation of effective analysis in Power BI. It defines how data is structured, interpreted, and ultimately how insights are derived.

Throughout this article, we’ve seen how:

Joins are used to reshape and prepare data by combining tables in Power Query
Relationships allow Power BI to connect tables logically, enabling efficient analysis without duplication
Schemas, particularly the star schema, provide a scalable structure for organizing fact and dimension tables
Data integrity (e.g., handling missing or invalid records like StudentID 105) directly impacts the quality of insights

Practical Takeaways

  • Use Power Query for data cleaning and controlled joins
  • Use Model View to define relationships and control how data interacts
  • Prefer 1:M relationships from dimension to fact tables
  • Default to single-direction filtering unless there is a clear need for bi-directional filtering
  • Adopt a star schema for most analytical scenarios
  • Treat missing or unmatched data as valuable signals, not just errors

Sprint 7 Retrospective: Quality Gates & Human Experience

Sprint 7 Retrospective: Quality Gates & Human Experience

TL;DR: Sprint 7 wired 118 V3 services to production REST API routes, built an E2E integration smoke test proving the 6-stage content pipeline works as a chain, enforced memory storage via ADR-032, tested OAuth2 credential rotation, and containerized ComfyUI/FFmpeg. Validated velocity: ~38 points (right-sized from 58). Nine consecutive sprint publishing passes. The gap: persona memory storage is still advisory-only despite three sprints of zero compliance.

Goal

Sprint 7 was the quality hardening sprint — transforming individually tested V3 services into a verified, integrated production system. Where Sprints 3-6 built capabilities, Sprint 7 proved they work together.

This is part of our sprint retrospective series:

Built

Sprint 7 delivered across 10 stories covering 5 capability areas:

V3 Service Wiring (OAS-123)

Wired 118 V3 services to api-server.mjs REST routes — the single largest integration task in the program. Each service got a dedicated REST endpoint, validated by source inspection tests that read the actual route file.

// E2E Pipeline — 6-stage artifact passing
interface StageResult {
  name: string;
  input: string | null;
  output: string | null;
  duration_ms: number;
  status: 'pass' | 'fail' | 'skip';
  error_code?: string;
  error_message?: string;
}

class PipelineExecutor {
  addStage(name: string, fn: StageFn): void;
  run(): Result<PipelineResult>;
}

E2E Integration Smoke Test (OAS-124)

First end-to-end test of the full content pipeline: Source → Script → Audio → Assembly → Quality Gate → RSS. Proves the “24/7 agency” vision works as a chain, not just individual units. PipelineStageValidator catches broken chains, duplicate stages, and contract violations.

Memory Enforcement (OAS-121)

ADR-032 advisory warnings at DONE and sprint close for persona memory storage. While advisory-only (not yet blocking), this establishes the infrastructure for Sprint 8 escalation.

OAuth2 Credential Rotation Testing (OAS-122)

AES-256-GCM encrypted credential store with PBKDF2 key derivation. Tests cover rotation without downtime, emergency revocation, and concurrent access patterns.

Containerization (OAS-120)

ComfyUI and FFmpeg containerized with GPU passthrough. The Dockerfile now compiles TypeScript during docker build — no manual npm run build needed before deployment.

Metrics

Metric Value
Stories Completed 10
Tickets Delivered 34
Tests Added 100+
V3 Services Wired 118
Planned Points 58
Validated Velocity ~38
Blocked Tickets 0
Publishing Streak 9 sprints

Program Cumulative Metrics

Sprint Focus Key Deliverable
Sprint 0-2 Infrastructure Platform foundation, memory, sourcing
Sprint 3 Audio TTS & voice management
Sprint 4 MOE Media Orchestration Engine
Sprint 5 YouTube Video pipeline
Sprint 6 Podcast RSS feed & distribution
Sprint 7 Quality Integration, E2E testing, service wiring
Total 8 sprints 118 services, 34 Sprint 7 tickets

Wrong

What Failed or Was Missing

  1. Persona memory: three sprints of zero compliance — Sprints 0, 4, and 7 all recorded zero persona-level memories despite ADR-032 advisory warnings. Advisory guidance is provably insufficient. This is the program’s most persistent process gap.

  2. Source inspection ≠ runtime validation — Sprint 7’s service wiring tests verify that route registrations exist in source code but cannot catch runtime binding errors, malformed responses, or middleware issues. HTTP integration tests are needed.

  3. Scope was right-sized from 58 to ~38 points — This is healthy (scope was pruned, not abandoned), but it reveals the planning estimates were 53% over-optimistic. Better estimation tooling would help.

  4. api-server.mjs is a monolith — 118 services wired to a single file. No domain separation, no route modules. This creates merge conflicts and cognitive overload for Sprint 8 UI work.

  5. Sprint 6 retro decision partially unimplemented — 3/4 Sprint 6 decisions were implemented, but persona memory enforcement remained advisory despite being flagged for escalation.

AI Participation

Sprint 7 was executed entirely by Claude (Opus 4.6) following the ORCHESTRATE Agile methodology via MCP enforcement. Ten AI personas contributed to the retrospective ceremony:

Persona Role Key Sprint 7 Insight
Pip Line DevOps 9-sprint publishing streak; TypeScript compiles in Docker now
Guard Ian Security Auth middleware must precede UI dashboard exposure
Api Endor Backend 118 services in one file is a maintenance risk; extract route modules
Archi Tect Architect Bottom-up architecture validated; integration before UI
React Ive Frontend Sprint 8 UI work depends on route module extraction
Aiden Orchestr AI Orchestration Advisory memory warnings are provably insufficient after 3 sprints
Content Curator Content Provenance metadata matured from 4 fields (Sprint 0) to 12+ fields
Tess Ter QA Source inspection catches wiring gaps but misses runtime errors
Scrum Ming Scrum Master Right-sizing from 58→38 pts preserved quality; validates ~38 pt velocity
Owen Pro Product Owner Sprint 8 scope must respect validated velocity, not aspirational targets

Lessons

Six lessons extracted from the Sprint 7 ceremony:

  1. Phased execution order matters — Infrastructure tickets (wiring, containers) must complete before integration tickets (E2E, smoke tests). Sprint 7 got this right.

  2. Right-sizing scope preserves delivery quality — Reducing from 58 to ~38 points was not a failure. It prevented the quality erosion that comes from overcommitment.

  3. Source inspection is necessary but not sufficient — Reading route registration code proves services are wired. It cannot prove they respond correctly at runtime.

  4. Persona memory requires mechanical enforcement — Three sprints of zero compliance (0, 4, 7) proves advisory-only ADR-032 warnings are ignored. Escalation to blocking gate is the only viable path.

  5. Bottom-up architecture validated — Building 118 individual services before the E2E pipeline test is correct. Integration tests need something to integrate.

  6. Provenance metadata maturation is a strength — Growing from 4 fields in Sprint 0 to 12+ fields in Sprint 7 (including temporal claims, data sensitivity checks) demonstrates continuous quality improvement.

Accountability

5 Decisions from Sprint 7 Retrospective

ID Decision Owner Target
D-S7-01 Complete auth middleware before UI dashboard work Guard Ian + Archi Tect Sprint 8
D-S7-02 Sprint 8 scope must respect ~38-point validated velocity Scrum Ming + Owen Pro Sprint 8
D-S7-03 Escalate memory enforcement from advisory to blocking gate Aiden Orchestr Sprint 8
D-S7-04 Add HTTP integration tests for api-server.mjs routes Tess Ter Sprint 8
D-S7-05 Extract route modules from monolithic api-server.mjs Api Endor Sprint 8

Sprint 6 Retro Disposition

Sprint 6 Decision Status Notes
Multi-voice podcast assembly Implemented OAS-064 delivered 6 tickets
RSS feed server Implemented OAS-062 delivered
Content style guides Implemented OAS-063 delivered
Persona memory escalation Partial Advisory only; escalated again as D-S7-03

3 Unresolved Tensions (Sprint 8 Planning Inputs)

  • Auth before UI: Guard Ian insists auth middleware must precede dashboard; React Ive wants to start UI immediately.
  • Route extraction timing: Api Endor wants route modules extracted before UI work; pragmatists want to defer to avoid refactoring risk.
  • Sprint 8 capacity: 108 points planned vs 38-point velocity — Sprint 9 overflow is inevitable; which stories get deferred?

Next

Sprint 7 closes the quality gates phase. Sprint 8 is the final planned sprint:

  • Sprint 8: UI Integration & Dashboard — single-pane-of-glass for the entire platform

The primary risk: Sprint 8 has ~108 points of stories (including 4 retro-decision carry-overs) against a validated velocity of ~38 points. Sprint 9 overflow is certain. The Sprint 8 prompt must prioritize: auth middleware (D-S7-01), route extraction (D-S7-05), and the unified dashboard (OAS-068) before secondary UI tabs.

Sprint 7’s honest assessment: integration-validated, UI-pending. The 118 services are wired and the E2E pipeline proves the chain works. Now it needs a human interface.

Provenance

Field Value
Sprint Sprint 7 — Quality Gates & Human Experience
Program ORCHESTRATE Marketing Platform Uplift
Author ORCHESTRATE AI Team (10 personas)
Methodology DD TDD — Documentation-Driven Test-Driven Development
Verified 2026-03-29
Test Evidence sprint7-devto-verify (T1), sprint7-blog-format (T2), sprint7-publish-repair (T3)
Publishing Reliability 100% across 9 sprint verifications
Data Sensitivity Checked — no API keys, credentials, or PII in post
Memory Citations OAS-086-T1 artifacts, OAS-086-T2 memories, OAS-086-T3 ceremony, OAS-086-T4 decisions
Blog Series Sprint 3, Sprint 4, Sprint 5, Sprint 6, Sprint 7 (this post)

Generated by ORCHESTRATE Agile Suite — Sprint 7 Quality Gates & Human Experience Retrospective

Claude Sonnet 4.5 Code Review Benchmark

Why benchmark LLMs for code review?

Most LLM benchmarks focus on code generation — writing new code from scratch, solving algorithmic puzzles, or completing functions. But code review is a fundamentally different task. A model that excels at generating code may perform poorly when asked to find subtle bugs in someone else’s code, assess security implications of a design choice, or evaluate whether a refactor actually improves maintainability.

Code review requires a different set of capabilities than code generation. When reviewing code, the model needs to:

  1. Understand intent from context. The model must infer what the code is supposed to do based on surrounding code, PR descriptions, commit messages, and file naming conventions — not from an explicit prompt.
  2. Identify what is wrong without being told what to look for. Unlike code generation where the task is clearly defined, code review is open-ended. The model needs to independently surface bugs, security issues, performance problems, and style violations.
  3. Distinguish real issues from false positives. This is arguably the most important skill. A model that flags everything is useless. A model that flags nothing is equally useless. The value is in high signal-to-noise ratio.
  4. Provide actionable feedback. Saying “this might have a bug” is not helpful. Explaining exactly what the bug is, why it matters, and how to fix it — that is useful code review.

Existing benchmarks like HumanEval, MBPP, and SWE-bench measure code generation and bug fixing. There is no widely accepted benchmark for code review quality. That is the gap we set out to fill.

We tested six leading LLMs on 54 real pull requests across five programming languages, measuring bug detection accuracy, security analysis, performance issue identification, false positive rates, and the actionability of review comments. This article presents the full methodology and results, with a particular focus on Claude Sonnet 4.5, Anthropic’s latest model at the time of testing.

Methodology

Benchmark integrity requires transparency about methodology. Here is exactly how we conducted this evaluation.

Test dataset: 54 real pull requests

We curated 54 pull requests from open-source repositories on GitHub. These were not synthetic examples or toy problems. Each PR was a real change submitted by a real developer to a real project. We selected PRs that had already been merged and had meaningful review history, so we could compare model outputs against actual human reviewer findings.

The PRs spanned five languages and multiple domains:

Language Number of PRs Repositories (examples)
Python 14 FastAPI, Django REST framework, scikit-learn
JavaScript 11 Next.js, Express, React ecosystem libraries
TypeScript 12 tRPC, Prisma, Angular
Java 9 Spring Boot, Apache Kafka, Elasticsearch
Go 8 Kubernetes, Docker, CockroachDB

We deliberately included a range of PR sizes:

  • Small PRs (under 100 lines changed): 16 PRs
  • Medium PRs (100-500 lines changed): 24 PRs
  • Large PRs (500+ lines changed): 14 PRs

Issue categories

Each PR was analyzed for issues across four categories:

  1. Bug detection — Logic errors, null pointer dereferences, race conditions, off-by-one errors, incorrect error handling, type mismatches
  2. Security analysis — SQL injection, cross-site scripting (XSS), authentication bypass, insecure deserialization, path traversal, hardcoded secrets, improper input validation
  3. Performance issues — N+1 queries, unnecessary memory allocations, algorithmic complexity problems, missing caching opportunities, blocking I/O in async contexts
  4. Code quality — Dead code, unclear naming, missing error propagation, violation of SOLID principles, inconsistent patterns within the codebase

Ground truth establishment

For each PR, we established ground truth through a three-step process:

  1. Existing review comments. We collected all issues identified by original human reviewers during the actual PR review process.
  2. Expert annotation. Two senior engineers (each with 10+ years of experience) independently reviewed each PR and documented all issues they found, including issues the original reviewers missed.
  3. Consensus labeling. The two annotators reconciled their findings. Any disagreements were discussed and resolved. The final ground truth for each PR was the union of confirmed issues, each labeled by category and severity (critical, major, minor).

This process yielded 247 confirmed issues across all 54 PRs: 89 bugs, 43 security issues, 52 performance problems, and 63 code quality concerns.

How we scored each model

Each model was given the same input for each PR: the full diff, the PR title and description, and relevant file context (up to 8,000 tokens of surrounding code for files touched by the PR). We used each model’s API directly with a standardized system prompt that instructed the model to perform a thorough code review and report issues by category.

For each model’s output, we measured:

  • True positive rate (recall) — Percentage of ground truth issues the model correctly identified
  • Precision — Percentage of the model’s flagged issues that were actual issues (inverse of false positive rate)
  • F1 score — Harmonic mean of precision and recall, providing a single balanced metric
  • Actionability score — Human-rated score (1-5) for how useful each review comment was, considering explanation quality, fix suggestions, and relevance

Two annotators independently scored each model’s output against ground truth. Inter-annotator agreement was 91%, and disagreements were resolved through discussion.

Transparency note

We are an independent review site. We are not affiliated with Anthropic, OpenAI, Google, or DeepSeek. We do not receive funding from any LLM vendor. We purchased API access to all models at standard pricing. Our goal is to provide useful, unbiased data to engineering teams making tooling decisions.

Models tested

We evaluated six models representing the current state of the art as of early 2026:

Model Provider Context Window Price (input / output per 1M tokens)
Claude Sonnet 4.5 Anthropic 200K $3.00 / $15.00
Claude Haiku 3.5 Anthropic 200K $0.80 / $4.00
GPT-4o OpenAI 128K $2.50 / $10.00
GPT-4o-mini OpenAI 128K $0.15 / $0.60
Gemini 2.5 Pro Google 1M $1.25 / $5.00
DeepSeek V3 DeepSeek 128K $0.27 / $1.10

All models were tested using their latest available versions as of February 2026. For Claude Sonnet 4.5, we tested both standard mode and extended thinking mode (which allows the model to reason through complex problems before responding). Temperature was set to 0 for all models to ensure reproducibility.

Results

Overall accuracy

The table below summarizes each model’s performance across all 247 confirmed issues in our dataset.

Model Recall Precision F1 Score Actionability (1-5)
Claude Sonnet 4.5 (extended thinking) 78.1% 82.4% 80.2% 4.3
GPT-4o 76.5% 78.9% 77.7% 4.1
Claude Sonnet 4.5 (standard) 74.9% 81.7% 78.2% 4.2
Gemini 2.5 Pro 73.3% 76.2% 74.7% 3.9
DeepSeek V3 68.4% 71.8% 70.1% 3.6
Claude Haiku 3.5 62.7% 74.5% 68.1% 3.5
GPT-4o-mini 59.1% 69.3% 63.8% 3.2

Key takeaways from overall results:

  • Claude Sonnet 4.5 with extended thinking achieved the highest F1 score (80.2%), edging out GPT-4o (77.7%) and standard Claude Sonnet 4.5 (78.2%).
  • The gap between extended thinking and standard mode for Claude Sonnet 4.5 was meaningful — an additional 3.2% recall and a 2 percentage point improvement in F1 score. Extended thinking particularly helped on complex multi-file bugs and architectural issues.
  • GPT-4o was competitive across the board and outperformed standard Claude Sonnet 4.5 on recall, though Claude maintained higher precision.
  • Gemini 2.5 Pro performed solidly in the middle tier, with particular strength in Go and Java codebases.
  • Budget models (Claude Haiku, GPT-4o-mini) were significantly behind the frontier models, but still useful for catching straightforward issues at a fraction of the cost.
  • DeepSeek V3 delivered impressive results for its price point, outperforming both budget models despite costing less than Claude Haiku.

Bug detection

Bug detection is the core value proposition of AI code review. We tested each model against the 89 confirmed bugs in our dataset.

Model Bugs Found Recall Precision Notable Strengths
Claude Sonnet 4.5 (extended) 73 82.0% 83.9% Race conditions, complex logic errors
GPT-4o 69 77.5% 79.5% Null safety, type mismatches
Claude Sonnet 4.5 (standard) 67 75.3% 82.7% Off-by-one errors, error handling
Gemini 2.5 Pro 63 70.8% 75.9% Concurrency bugs in Go/Java
DeepSeek V3 57 64.0% 70.4% Python-specific patterns
Claude Haiku 3.5 51 57.3% 73.6% Common null checks
GPT-4o-mini 47 52.8% 67.1% Simple pattern matching

Example: Race condition caught by Claude Sonnet 4.5 but missed by other models.

In a Go PR modifying a concurrent map access pattern in a Kubernetes controller, the following code was introduced:

func (c *Controller) reconcile(ctx context.Context, key string) error {
    obj, exists := c.cache[key]
    if !exists {
        return nil
    }

    // Process the object
    result, err := c.process(ctx, obj)
    if err != nil {
        return err
    }

    c.cache[key] = result
    return nil
}

Claude Sonnet 4.5 (with extended thinking) identified that c.cache was a map[string]Object accessed from multiple goroutines without synchronization. Its review comment explained the race condition, referenced Go’s memory model, and suggested using a sync.RWMutex or sync.Map. GPT-4o flagged it as a “potential concurrency concern” but did not explain the specific risk or provide a fix. Other models missed it entirely.

Example: Null pointer dereference caught by most models.

In a TypeScript PR:

async function getUserProfile(userId: string) {
  const user = await prisma.user.findUnique({ where: { id: userId } });
  const subscription = user.subscription; // user could be null
  return { name: user.name, plan: subscription?.tier ?? 'free' };
}

Every model except GPT-4o-mini caught the potential null dereference on user.subscription when findUnique returns null. This type of straightforward null safety check is where all models perform reasonably well — it is the more nuanced bugs where differentiation appears.

Security analysis

Security vulnerabilities are high-stakes findings. A missed SQL injection or authentication bypass can lead to a data breach. We tested each model against 43 confirmed security issues.

Model Issues Found Recall Precision Strengths
Claude Sonnet 4.5 (extended) 37 86.0% 88.1% Auth bypass, complex injection chains
GPT-4o 35 81.4% 83.3% XSS, SSRF, common OWASP patterns
Claude Sonnet 4.5 (standard) 34 79.1% 87.2% Input validation, deserialization
Gemini 2.5 Pro 32 74.4% 78.0% SQL injection, path traversal
DeepSeek V3 27 62.8% 71.1% Basic injection patterns
Claude Haiku 3.5 25 58.1% 75.8% Hardcoded secrets, obvious XSS
GPT-4o-mini 22 51.2% 66.7% Simple pattern-based detection

SQL injection detection. All frontier models (Claude Sonnet 4.5, GPT-4o, Gemini 2.5 Pro) caught direct SQL injection vulnerabilities with 100% recall. Differentiation appeared with indirect injection — cases where user input passed through multiple function calls before reaching a query. Claude Sonnet 4.5 with extended thinking traced data flow across three function boundaries in a Django application to identify an injection vector. GPT-4o caught the same issue when it spanned two functions but missed it at three. Gemini 2.5 Pro missed multi-hop injection entirely.

XSS detection. GPT-4o had a slight edge in detecting cross-site scripting vulnerabilities, catching 10 of 11 XSS issues compared to Claude Sonnet 4.5’s 9 of 11. GPT-4o was particularly good at identifying DOM-based XSS in React applications where dangerouslySetInnerHTML was used with insufficiently sanitized input.

Authentication bypass. Claude Sonnet 4.5 with extended thinking stood out in detecting authentication bypass vulnerabilities. In one Spring Boot PR, it identified that a new API endpoint was missing the @PreAuthorize annotation that all sibling endpoints had, and correctly flagged this as a critical security issue. It also caught a subtle IDOR (Insecure Direct Object Reference) vulnerability where user ID validation was present in the controller but missing in a newly added service method that was called from a different controller.

Performance issues

Performance bugs are often subtle and require understanding of runtime characteristics. We tested each model against 52 confirmed performance issues.

Model Issues Found Recall Precision
Claude Sonnet 4.5 (extended) 38 73.1% 79.2%
GPT-4o 37 71.2% 77.1%
Claude Sonnet 4.5 (standard) 35 67.3% 79.5%
Gemini 2.5 Pro 34 65.4% 73.9%
DeepSeek V3 28 53.8% 68.3%
Claude Haiku 3.5 24 46.2% 70.6%
GPT-4o-mini 21 40.4% 63.6%

N+1 query detection. This was the most commonly planted performance issue, appearing in 12 PRs across Python (Django/SQLAlchemy), TypeScript (Prisma), and Java (Hibernate). Claude Sonnet 4.5 caught 11 of 12 N+1 patterns, GPT-4o caught 10, and Gemini 2.5 Pro caught 9. The one N+1 issue that Claude missed was deeply nested inside a recursive function that loaded child entities conditionally — a pattern that all models struggled with.

Algorithmic complexity. We included 8 PRs with suboptimal algorithm choices (for example, using nested loops where a hash map lookup would reduce O(n^2) to O(n)). Claude Sonnet 4.5 with extended thinking caught 7 of 8, often providing the improved algorithm in its review comment. GPT-4o caught 6 of 8 but typically only mentioned “this could be optimized” without providing the specific improvement.

Memory leaks and resource management. In Go and Java PRs involving unclosed resources (database connections, file handles, HTTP response bodies), Claude and GPT-4o performed similarly, each catching roughly 75% of resource leak issues. Gemini 2.5 Pro was notably strong on Go-specific resource management, catching defer misuse and context cancellation issues that other models missed.

False positive rate

False positives are the silent killer of AI code review adoption. If a tool generates too many incorrect findings, developers learn to ignore it entirely, defeating its purpose. Lower false positive rates are better.

Model Total Comments True Positives False Positives FP Rate
Claude Sonnet 4.5 (standard) 226 185 41 18.1%
Claude Sonnet 4.5 (extended) 234 193 41 17.5%
Claude Haiku 3.5 195 155 40 20.5%
GPT-4o 259 205 54 20.8%
DeepSeek V3 232 175 57 24.6%
Gemini 2.5 Pro 268 198 70 26.1%
GPT-4o-mini 237 162 75 31.6%

Claude Sonnet 4.5 produced the fewest false positives of any model tested. Both in standard and extended thinking modes, Claude maintained a false positive rate below 19%, compared to GPT-4o at 20.8% and Gemini 2.5 Pro at 26.1%.

The most common types of false positives across all models were:

  1. Style preferences disguised as bugs (32% of all FPs) — flagging valid but unconventional code patterns as incorrect
  2. Missing context (28% of all FPs) — identifying an “issue” that was actually handled elsewhere in the codebase
  3. Overly cautious security warnings (22% of all FPs) — flagging already-sanitized input as a security risk
  4. Incorrect language semantics (18% of all FPs) — misunderstanding language-specific behavior and flagging correct code as buggy

Claude Sonnet 4.5’s lower false positive rate appears to stem from two factors. First, it produces fewer style-based false positives — it is more likely to note a stylistic observation without categorizing it as a bug. Second, its extended thinking mode allows it to reason through potential false positives before reporting them, effectively self-filtering its own output.

Response quality

Beyond raw accuracy, we evaluated the quality of each model’s review comments on a 1-5 scale across three dimensions:

Model Explanation Clarity Fix Quality Actionability Overall
Claude Sonnet 4.5 (extended) 4.5 4.4 4.3 4.3
Claude Sonnet 4.5 (standard) 4.4 4.2 4.2 4.2
GPT-4o 4.2 4.0 4.1 4.1
Gemini 2.5 Pro 3.9 3.9 3.9 3.9
DeepSeek V3 3.6 3.5 3.6 3.6
Claude Haiku 3.5 3.5 3.4 3.5 3.5
GPT-4o-mini 3.3 3.1 3.2 3.2

Explanation clarity. Claude Sonnet 4.5 consistently provided the most thorough explanations. When identifying a race condition, for example, it would explain the specific interleaving that causes the bug, reference the relevant memory model, and describe the potential consequences. GPT-4o’s explanations were accurate but more concise, sometimes to the point of being insufficient for junior developers to understand the issue.

Fix quality. Claude Sonnet 4.5 with extended thinking provided the most complete and correct fix suggestions. In 87% of cases where it suggested a fix, the fix was directly applicable without modification. GPT-4o’s fix rate was 79%, and Gemini 2.5 Pro’s was 74%. Budget models frequently suggested fixes that were syntactically correct but semantically incomplete — fixing the immediate symptom without addressing the underlying issue.

Actionability. This is a composite score reflecting whether a developer could take immediate action based on the review comment. Claude Sonnet 4.5 scored highest here because its comments typically included: (1) a clear description of the problem, (2) why it matters, (3) a suggested fix, and (4) a severity assessment. GPT-4o often omitted the severity assessment, and budget models frequently omitted the fix suggestion.

Cost analysis

Performance must be weighed against cost. We measured the total API cost for reviewing all 54 PRs with each model.

Model Total Cost (54 PRs) Avg Cost per PR Accuracy (F1) Cost-Effectiveness (F1 per $)
GPT-4o-mini $1.87 $0.03 63.8% 34.1
DeepSeek V3 $3.42 $0.06 70.1% 20.5
Claude Haiku 3.5 $5.94 $0.11 68.1% 11.5
Gemini 2.5 Pro $9.18 $0.17 74.7% 8.1
Claude Sonnet 4.5 (standard) $18.36 $0.34 78.2% 4.3
GPT-4o $21.06 $0.39 77.7% 3.7
Claude Sonnet 4.5 (extended) $27.54 $0.51 80.2% 2.9

Cost-effectiveness analysis. If your sole metric is accuracy per dollar, GPT-4o-mini and DeepSeek V3 win. But this metric is misleading for code review because the cost of a missed bug in production far exceeds the cost of a review API call. A single production incident can cost thousands of dollars in engineering time, and a security breach can cost orders of magnitude more.

For teams that need maximum accuracy, Claude Sonnet 4.5 with extended thinking costs roughly $0.51 per PR review. For a team merging 50 PRs per week, that is approximately $102 per month — less than half the cost of a single hour of senior engineer time. At this price point, the cost of the API is negligible compared to the value of catching even one additional critical bug per month.

The sweet spot for most teams is Claude Sonnet 4.5 in standard mode or GPT-4o, which deliver frontier-level accuracy at roughly $0.34 to $0.39 per PR. Teams on tight budgets should consider DeepSeek V3, which delivers surprisingly strong results at $0.06 per PR.

A practical tiered approach is to use a budget model (Claude Haiku or GPT-4o-mini) for initial screening on all PRs and escalate to a frontier model (Claude Sonnet 4.5 or GPT-4o) only for PRs that touch security-sensitive code or exceed a certain size threshold. This can reduce costs by 60-70% while maintaining high accuracy where it matters most.

Claude Sonnet 4.5 deep dive

Since Claude Sonnet 4.5 topped our overall benchmark, it warrants a deeper examination of its strengths, weaknesses, and ideal use cases.

Extended thinking for complex reviews

Claude Sonnet 4.5’s extended thinking capability is its most distinctive feature for code review. When enabled, the model takes additional time to reason through the code before generating its review. This manifests as a visible “thinking” phase where the model breaks down the problem, considers multiple interpretations, and evaluates potential issues before committing to its output.

In our testing, extended thinking added 15-30 seconds to review time but delivered measurable improvements:

  • +6.7% recall on bugs compared to standard mode (82.0% vs 75.3%)
  • +6.9% recall on security issues (86.0% vs 79.1%)
  • +5.8% recall on performance issues (73.1% vs 67.3%)
  • Lower false positive rate (17.5% vs 18.1%)

The improvement was most pronounced on complex, multi-file issues that require reasoning across multiple code paths. For simple issues like null checks and missing error handling, extended thinking provided minimal benefit over standard mode.

Strengths

  1. Lowest false positive rate. Claude Sonnet 4.5 consistently produced the cleanest output with the fewest incorrect findings. This matters enormously for adoption — developers who trust the tool will actually read and act on its findings.

  2. Best explanation quality. Claude’s review comments read like they were written by a thoughtful senior engineer. They explain not just what is wrong, but why it matters and what the consequences could be. This is particularly valuable for teams with junior developers who learn from review feedback.

  3. Strongest on nuanced bugs. Race conditions, subtle state management issues, and cross-function data flow bugs were Claude’s specialty. These are precisely the bugs that are hardest for human reviewers to catch and most expensive when they reach production.

  4. Consistent across languages. While some models showed significant performance variation across languages (Gemini was notably stronger on Go, DeepSeek on Python), Claude Sonnet 4.5 delivered consistent results across all five tested languages.

Weaknesses

  1. Slower than GPT-4o. Claude Sonnet 4.5 in standard mode was roughly 20% slower than GPT-4o for equivalent-length reviews. With extended thinking enabled, it was 40-60% slower. For teams that prioritize speed (for example, for real-time review-as-you-type workflows), this latency may matter.

  2. Higher cost. Claude Sonnet 4.5 with extended thinking was the most expensive option in our benchmark at $0.51 per PR. While we argue this cost is justified by accuracy gains, budget-constrained teams may find better value elsewhere.

  3. Occasional verbosity. Claude Sonnet 4.5 sometimes provided longer explanations than necessary for simple issues. A null check does not need three paragraphs of explanation. This verbosity can increase noise in review comments, partially offsetting the low false positive rate.

  4. Weaker on DOM-specific XSS. GPT-4o outperformed Claude on React-specific XSS detection, suggesting Claude’s training may have slightly less emphasis on frontend security patterns.

Best use cases for Claude Sonnet 4.5

Based on our benchmark results, Claude Sonnet 4.5 is the best choice for:

  • Security-critical codebases where missing a vulnerability is unacceptable
  • Backend services handling financial transactions, authentication, or sensitive data
  • Teams with mixed experience levels where detailed explanations help junior developers learn
  • Complex microservice architectures where bugs span multiple services and require cross-file reasoning
  • Final review gate in a tiered review pipeline where a budget model handles the first pass

Claude Sonnet 4.5 is not the best choice for:

  • High-volume, low-stakes reviews where cost and speed matter more than depth
  • Real-time code suggestions where latency needs to be under 2 seconds
  • Simple linting and formatting checks where a rule-based tool is faster and cheaper

Practical recommendations

Based on the full benchmark results, here are our recommendations for different team profiles.

Best model by use case

Use Case Recommended Model Why
Maximum accuracy Claude Sonnet 4.5 (extended) Highest F1 score, lowest FP rate
Best value for money Claude Sonnet 4.5 (standard) Near-top accuracy at moderate cost
Budget-conscious teams DeepSeek V3 Strong results at very low cost
Speed-critical workflows GPT-4o Fastest response with frontier accuracy
Open-source projects Claude Haiku or GPT-4o-mini Free tiers and low cost at scale
Security-focused review Claude Sonnet 4.5 (extended) Highest security issue detection rate

Cost-optimized configurations

Tiered review pipeline. Use Claude Haiku ($0.11/PR) as a first-pass reviewer on all PRs. For PRs that touch authentication, payment, or infrastructure code, escalate to Claude Sonnet 4.5 with extended thinking ($0.51/PR). This reduces average cost to roughly $0.15/PR while maintaining high accuracy on critical paths.

Size-based routing. Use GPT-4o-mini ($0.03/PR) for PRs under 100 lines. Use Claude Sonnet 4.5 standard ($0.34/PR) for PRs between 100-500 lines. Use Claude Sonnet 4.5 extended ($0.51/PR) for PRs over 500 lines. This matches model capability to review complexity.

Language-specific routing. For Go codebases, Gemini 2.5 Pro offers strong performance at lower cost than Claude or GPT-4o. For Python-heavy projects, DeepSeek V3 provides good value. For polyglot codebases, Claude Sonnet 4.5 offers the most consistent cross-language performance.

When to use expensive vs. budget models

Use a frontier model (Claude Sonnet 4.5, GPT-4o) when:

  • The PR touches security-sensitive code (authentication, authorization, input validation, cryptography)
  • The PR modifies financial calculations or billing logic
  • The PR introduces new architectural patterns or public APIs
  • The PR changes concurrency or distributed systems code
  • The PR is from an external contributor or junior developer

Use a budget model (Claude Haiku, GPT-4o-mini, DeepSeek V3) when:

  • The PR is a dependency update or version bump
  • The PR only changes tests or documentation
  • The PR is small (under 50 lines) with straightforward changes
  • The PR has already been thoroughly reviewed by a human
  • The PR is for internal tooling or non-production code

How tools use these models

Benchmarking raw LLM capabilities is useful, but most teams interact with these models through code review tools that add structure, context, and workflow integration on top of the base model.

CodeRabbit’s model selection

CodeRabbit supports multiple LLM backends and selects models dynamically based on the review context. It uses frontier models like Claude Sonnet 4.5 and GPT-4o for security-sensitive analysis and complex multi-file reviews, while routing simpler checks to faster, cheaper models. CodeRabbit also adds its own context layer — pulling in PR descriptions, linked issues, repository-level patterns, and learned team preferences — which can improve raw model performance by 10-15% compared to sending the diff alone.

CodeRabbit’s configuration allows teams to specify which model to prioritize, and its natural language instruction system means you can tell it to “focus on security issues and ignore style suggestions” to reduce noise. For teams that want the accuracy benefits of Claude Sonnet 4.5 without managing the API directly, CodeRabbit is one of the most accessible options.

PR-Agent’s model support

PR-Agent, the open-source AI code review tool by Qodo, supports Claude models through the Anthropic API. You can configure PR-Agent to use Claude Sonnet 4.5 as its primary review model, and it handles prompt construction, diff chunking, and review comment formatting automatically.

PR-Agent’s advantage is self-hosting — your code never leaves your infrastructure. This matters for teams with strict data handling policies. The trade-off is that you need to manage API keys, handle rate limits, and tune prompts yourself. PR-Agent’s default prompts are optimized for GPT-4o, so switching to Claude may require prompt adjustments for optimal results.

Building your own with APIs

For teams with specific requirements, building a custom review pipeline using the Claude API directly offers the most flexibility. The typical architecture involves:

  1. Webhook listener — A GitHub or GitLab webhook that triggers on PR events
  2. Context builder — A service that extracts the diff, gathers relevant file context, and constructs the prompt
  3. Model call — A call to the Anthropic API with the review prompt and code context
  4. Comment formatter — Parsing the model’s response and posting it as inline review comments

The Anthropic API supports streaming, which allows you to show review comments as they are generated rather than waiting for the full response. For large PRs, you can split the diff into chunks and review each chunk in parallel, then aggregate results.

A basic implementation takes 2-3 days for a senior engineer. Production-grade implementations that handle rate limiting, retry logic, cost tracking, and review comment deduplication typically take 1-2 weeks.

Conclusion

Claude Sonnet 4.5 earned the top position in our benchmark by delivering the highest overall accuracy, the lowest false positive rate, and the most actionable review comments. Its extended thinking capability provides a meaningful accuracy boost for complex reviews, and its consistent performance across languages makes it a reliable choice for polyglot codebases.

However, model selection for code review is not one-size-fits-all. GPT-4o is a strong alternative with faster response times. DeepSeek V3 offers remarkable value for budget-conscious teams. And for teams that want to avoid managing LLM infrastructure entirely, tools like CodeRabbit and PR-Agent abstract away model selection and provide turnkey code review with the best models under the hood.

The most important takeaway from our benchmark is that all frontier LLMs are now good enough to provide meaningful value in code review. The differences between Claude Sonnet 4.5, GPT-4o, and Gemini 2.5 Pro are real but incremental. The much larger gap is between using any AI code review and using none at all. Teams that are not yet using AI-assisted code review are leaving significant value on the table — regardless of which model they choose.

We will update this benchmark as new models are released. Our next round of testing will include Claude Opus, GPT-5, and Gemini 2.5 Ultra when they become available.

Frequently Asked Questions

Is Claude Sonnet 4.5 good for code review?

Yes. In our benchmark across 50+ real PRs, Claude Sonnet 4.5 ranked among the top models for code review accuracy, particularly excelling at bug detection and producing fewer false positives than competing models. Its extended thinking capability helps with complex architectural analysis.

How does Claude compare to GPT-4o for code review?

Claude Sonnet 4.5 and GPT-4o perform comparably for code review overall. Claude excels at nuanced bug detection and produces more actionable suggestions, while GPT-4o is slightly faster. Both significantly outperform smaller models for complex review tasks.

Which LLM is best for code review?

Based on our benchmark, Claude Sonnet 4.5 and GPT-4o are the top choices for code review accuracy. Claude Sonnet 4.5 offers the best balance of accuracy and cost. For teams prioritizing speed over depth, GPT-4o-mini or Claude Haiku provide good results at lower cost.

What tools use Claude for code review?

CodeRabbit uses Claude models as one of its AI backends. PR-Agent supports Claude through the Anthropic API. You can also build custom review workflows using the Claude API directly. Cursor and Windsurf use Claude for inline code suggestions.

Originally published at aicodereview.cc

Zod vs Typia vs AJV — I Built a Vite Plugin That Makes Zod 60x Faster With Zero Code Changes

When I first released Zod AOT, it required wrapping every schema with compile():

import { compile } from "zod-aot";

const UserSchema = compile(
  z.object({
    name: z.string().min(1),
    email: z.email(),
  })
);

The #1 feedback was: “I don’t want to change my code.”

Fair. So I removed that requirement. Zod AOT now has an autoDiscover mode — a Vite plugin that finds your Zod schemas at build time, compiles them into optimized validators, and replaces them in-place. Your schema files stay pure Zod. No imports from zod-aot. No wrappers. Just this:

// vite.config.ts
import zodAot from "zod-aot/vite";

export default defineConfig({
  plugins: [zodAot({ autoDiscover: true })],
});

That’s it. Every exported Zod schema in your project gets AOT-compiled at build time.

I also added Typia and AJV to the benchmarks, built a two-phase “Fast Path” validator, and shipped a diagnostic CLI. Here’s what changed.

autoDiscover: How It Works

The challenge is detecting Zod schemas without any marker in the source code. There’s no compile() call, no special import — just plain Zod:

// src/schemas.ts — plain Zod, no Zod AOT import
import { z } from "zod";

export const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.email(),
  age: z.number().int().min(0).max(150),
  role: z.enum(["admin", "editor", "viewer"]),
});

export const UpdateUserSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  email: z.email().optional(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(["admin", "editor", "viewer"]).optional(),
});

The plugin detects and compiles these in four steps:

Step 1: Regex Pre-filter

Before doing any work, the plugin checks if the file contains a runtime Zod import:

import { z } from "zod"   ✓  (runtime — schema definitions)
import type { z } from "zod"  ✗  (type-only — skip)

Files without a runtime Zod import are skipped entirely. This keeps build times fast — most files in your project aren’t schema files.

Step 2: Execute and Inspect Exports

The plugin loads the source file at build time using jiti and inspects each export:

function isZodSchema(value: unknown): boolean {
  if (typeof value !== "object" || value === null || !("_zod" in value))
    return false;
  const zod = (value as Record<string, unknown>)["_zod"];
  return typeof zod === "object" && zod !== null && "def" in zod;
}

Every Zod schema has a _zod.def structure — this is the internal definition that describes the schema’s type, checks, and children. If an export has this marker, it’s a Zod schema.

Step 3: Compile to Optimized Validators

Each discovered schema goes through the Zod AOT compilation pipeline:

Zod Schema → Extract IR → Generate Code → Wrap in IIFE

The pipeline walks the schema tree, extracts an intermediate representation, and generates flat, inlined validation code — the same process as before, but triggered automatically.

Step 4: AST-Based Source Replacement

The plugin uses acorn to parse expression boundaries in the source code:

// Before (source)
export const CreateUserSchema = z.object({ ... });

// After (build output)
export const CreateUserSchema = /* @__PURE__ */ (() => {
  // ... generated validation code ...
  var __w = Object.create(originalSchema);
  __w.parse = function(input) { /* optimized */ };
  __w.safeParse = __validate;
  __w.schema = originalSchema;
  return __w;
})();

The Object.create(originalSchema) is key — it preserves the full Zod API. Your compiled schema still has .shape, .keyof(), .pick(), .merge(), and everything else. Framework code that inspects schema metadata (like tRPC or React Hook Form resolvers) continues to work.

Build Output

With verbose: true, you see what happened:

[zod-aot] Auto-discovering: src/schemas.ts (4 Zod exports found)
[zod-aot]   ✓ CreateUserSchema
[zod-aot]   ✓ UpdateUserSchema
[zod-aot]   ✓ ListUsersSchema
[zod-aot]   ✓ UserIdSchema
[zod-aot] Build summary: 4/4 schemas optimized across 1 file(s)

Scoping

Not every file should be auto-discovered — you probably don’t want to execute test fixtures or files with side effects at build time. Use include and exclude:

zodAot({
  autoDiscover: true,
  include: ["src/schemas"],
  exclude: ["test", "mock"],
});

Two-Phase Validation: The Fast Path

The prior version of Zod AOT generated error-collecting validators — they always created an issues array and tracked every validation failure. That’s necessary for error reporting, but it’s overhead when the input is valid.

In production, most inputs are valid. So I added a Fast Path.

Phase 1: Boolean Expression Chain

For valid inputs, the entire schema is validated with a single boolean expression:

// Generated Fast Path for CreateUserSchema
typeof input==="object"&&input!==null&&!Array.isArray(input)&&
typeof input.name==="string"&&input.name.length>=1&&input.name.length<=100&&
typeof input.email==="string"&&__re0.test(input.email)&&
typeof input.age==="number"&&!Number.isNaN(input.age)&&
  Number.isSafeInteger(input.age)&&input.age>=0&&input.age<=150&&
(input.role==="admin"||input.role==="editor"||input.role==="viewer")

If this evaluates to true, the function returns { success: true, data: input } immediately.

Zero allocations. No issues array. No error objects. No path arrays. Just a boolean chain that short-circuits on the first failure.

Regex patterns and enum Sets are pre-compiled in a preamble:

var __re0 = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
// Small enums (≤3 values) use inline === checks instead of Set.has()

Phase 2: Error-Collecting Path (Slow Path)

If the Fast Path fails — meaning the input has at least one issue — the full error-collecting validator runs. This creates the standard { success: false, error: { issues: [...] } } response with codes, paths, and messages.

The key insight: the Slow Path only runs when it’s actually needed. For hot API endpoints where 95%+ of requests pass validation, the Fast Path is all that executes.

Fast Path Eligibility

Not every schema qualifies for a Fast Path. Schemas with .transform(), .refine(), .default(), .coerce(), or .catch() are ineligible because they need to modify the input or run opaque functions. The check command (see below) tells you which schemas qualify.

The 5-Way Benchmark

I added Typia and AJV to the benchmark suite. All five validators parse the same data with equivalent schemas:

  • Zod v3: Interpreter model, walks schema tree on every call
  • Zod v4: New architecture with JIT object compilation via new Function()
  • Zod AOT: Build-time compilation with two-phase validation
  • Typia: Compile-time code generation via TypeScript transformer
  • AJV: JSON Schema validator with ahead-of-time compilation

Environment: Node.js v22, Apple M-series, Vitest bench, 1M+ iterations with warmup.

Full Results

Schema Zod v3 Zod v4 Zod AOT Typia AJV
String min/max 8.3M 5.6M 10.6M 10.5M 8.8M
Enum 7.6M 9.3M 10.2M 10.8M 10.7M
Medium object (valid) 1.3M 1.7M 5.2M 7.0M 4.8M
Medium object (invalid) 365K 63K 471K 2.1M 4.7M
Large object — 100 items 8.6K 11.6K 627K 808K 89K
Discriminated union (3) 2.3M 3.0M 10.2M 10.6M 5.4M
Recursive tree — 121 nodes 23K 103K 733K 1.4M 251K
Set (20 items) 980K 461K 7.6M — —
Map (20 entries) 479K 235K 5.1M — —
Event log (mixed types) 263K 473K 4.5M — —
Partial fallback — 50 items 18K 30K 238K — —

ops/sec — higher is better. “—” = type not supported by the library.

What the numbers tell us:

Primitives — All AOT approaches converge around ~10.5M ops/sec. This is the V8 ceiling for simple type checks. No meaningful difference between Zod AOT, Typia, and AJV.

Objects — Typia leads (1.3x over Zod AOT on medium, 1.2x on large). But the gap narrows as objects grow — the Fast Path’s single boolean chain scales well. AJV collapses on large objects (9x slower than Typia at 100 items). Zod v4 is 3-54x slower than both.

Invalid inputs — AJV dominates the error path (4.7M ops/s) with minimal error construction. Zod v4 is slowest at 63K due to rich structured errors. In practice, most production inputs are valid — the Fast Path handles those.

Collections (Set/Map) — Zod AOT’s exclusive territory. Typia doesn’t validate Set or Map contents. AJV doesn’t support JS-native types. Zod AOT is 16-22x faster than Zod v4 here.

Recursive — Typia leads 1.9x over Zod AOT on deep trees. Zod AOT currently uses z.lazy() fallback for recursive references — improving this is on the roadmap.

Partial fallback — Schemas with .transform() can’t be fully AOT-compiled, but Zod AOT compiles everything it can and delegates the rest to Zod. Even partial compilation delivers 2.6-8x over Zod v4.

The Scoreboard

Category Winner Runner-up
Primitives Tie (all AOT ≈ 10.5M) —
Objects (valid) Typia Zod AOT (within 1.2x)
Objects (invalid) AJV Typia
Collections (Set/Map) Zod AOT (only competitor)
Discriminated unions Tie (Typia ≈ Zod AOT) —
Recursive Typia Zod AOT (1.9x gap)
Partial compilation Zod AOT (only competitor)

Zod AOT sits in a unique position: near-Typia performance with full Zod API compatibility. You don’t need type-level tags, a different schema DSL, or JSON Schema. You keep z.object(), z.email(), .transform(), .refine() — and the build plugin handles the rest.

The check Command: Know Your Coverage

Not sure what percentage of your schemas will be compiled? Run check:

npx zod-aot check src/schemas.ts
validateUser — 100% compiled (8/8 nodes) | Fast Path: eligible
  └─ ✓ object
     ├─ ✓ string .name
     ├─ ✓ string .email
     ├─ ✓ number .age
     ├─ ✓ enum .role
     ├─ ✓ boolean .isActive
     ├─ ✓ array .tags
     │  └─ ✓ string .tags[]
     └─ ✓ nullable .bio
        └─ ✓ string .bio

validateOrder — 80% compiled (4/5 nodes) | Fast Path: ineligible
  └─ ✓ object
     ├─ ✓ string .orderId
     ├─ ✓ number .amount
     ├─ ✓ enum .currency
     └─ ✗ fallback .slug (transform)
           hint: Extract transform into a separate post-processing step

Each schema gets a tree view showing:

  • Which nodes are fully compiled (✓) vs falling back to Zod (✗)
  • Compilation coverage percentage
  • Fast Path eligibility
  • Actionable hints for improving coverage

CI Integration

# Fail if compilation coverage drops below 80%
npx zod-aot check src/schemas/ --fail-under 80 --json

The --json flag outputs structured data for CI pipelines:

{
  "exportName": "validateOrder",
  "coverage": { "total": 5, "compilable": 4, "percent": 80 },
  "fastPath": { "eligible": false, "blocker": "fallback (transform)" },
  "fallbacks": [{
    "reason": "transform",
    "path": ".slug",
    "hint": "Extract transform into a separate post-processing step"
  }]
}

Framework Integration

autoDiscover works at the build layer — framework code doesn’t know or care:

tRPC: Schemas auto-compiled, router unchanged.

// router.ts — no changes needed
const appRouter = router({
  createUser: publicProcedure
    .input(CreateUserSchema)  // ← this is now AOT-compiled
    .mutation(({ input }) => db.users.create(input)),
});

Hono: Middleware validation auto-optimized.

app.post("/users", zValidator("json", CreateUserSchema), (c) => {
  // CreateUserSchema is AOT-compiled at build time
  const data = c.req.valid("json");
});

React Hook Form: Resolver schemas auto-compiled.

const { register } = useForm({
  resolver: zodResolver(CreateUserSchema), // AOT-compiled
});

The Object.create() wrapper ensures that framework code reading .shape, .keyof(), or other Zod metadata still works — the compiled schema inherits from the original.

What I Learned from the Competition

Typia is the performance king for objects and recursive structures. Its compile-time TypeScript transformer generates monolithic validation functions with zero runtime overhead. But:

  • It requires type-level tags (tags.MinLength<3>) instead of method chaining — a different mental model
  • It doesn’t validate Set or Map contents
  • No bigint support
  • No .transform() or .refine() — it’s a validator, not a parser

AJV has the best error-path performance and the broadest ecosystem (JSON Schema is a standard). But:

  • JSON Schema doesn’t support JavaScript-native types (Set, Map)
  • Verbose schema definitions — no chaining, no composition
  • No transforms or pipes — declarative validation only

Zod AOT fills the gap: Zod’s composable API, near-Typia performance, full JavaScript type support, and partial compilation for schemas that mix validation with transformation.

Get Started

npm install zod-aot zod@^4
// vite.config.ts — that's the entire setup
import zodAot from "zod-aot/vite";

export default defineConfig({
  plugins: [zodAot({ autoDiscover: true })],
});

Your existing Zod schemas are now AOT-compiled. No code changes.

Also works with webpack, esbuild, Rollup, Rolldown, rspack, and Bun via unplugin.

Methodology

All benchmarks use safeParse() on pre-created schema instances, measured with Vitest bench (1M+ iterations, with warmup). Numbers are steady-state throughput — JIT is already compiled after the first call. Typia uses createValidate<T>(). AJV uses ajv.compile(schema). Invalid-input benchmarks use all-fields-invalid payloads to stress the error path.

  • Environment: Node.js v22, Apple M-series
  • Libraries: Zod v3.24, Zod v4, Zod AOT 0.14, Typia 12, AJV 8
  • Raw data: benchmarks/ in the zod-aot repo

SQL Assertions, ANSI join, and ORA-08697

In the previous post on consistency boundaries, we saw that an updatable join view can hide a write-skew anomaly when the developer assumes the consistency boundary is a single row, even though there are actually two underlying rows and only one is locked. I wanted to see how the new SQL Assertions introduced in “Oracle AI Database 26ai Release 23.26.1.0.0” handle this, since they use a different locking mechanism.

TL;DR: they handle it correctly, but without ANSI joins.

Without assertions, for an employee with a salary of 1000 and a commission of 100, two concurrent users could each add 42 to both values because they couldn’t see each other’s changes, and updating one column didn’t lock the other. I then added a SQL assertion that doesn’t allow the commission to exceed 1150, so that only one session can add 42. This assertion works as expected, waiting on the other transaction before checking the sum:

CREATE ASSERTION salary_plus_commission_le_1150
CHECK (
  NOT EXISTS (
    SELECT 'violation'
    FROM emp_salary s, emp_commission c
    WHERE s.empno = c.empno
      AND s.salary + c.commission > 1150
  )
);

When the first transaction committed, the second transaction detected the write skew:

ERROR at line 1:
ORA-08601: SQL assertion (FRANCK.SALARY_PLUS_COMMISSION_LE_1150) violated.

I’m writing this blog post because I had trouble finding the correct declaration for this assertion, and I hope it helps others. My first attempt was simply:

CREATE ASSERTION salary_plus_commission_le_1150
CHECK (
  NOT EXISTS (
    SELECT 'violation'
    FROM emp_salary s join emp_commission c USING (empno)
    WHERE s.salary + c.commission > 1150
  )
);

This joins the two tables and raises a violation is the sum of salary and commission is higher than 1150, but the CREATE ASSERTION failed with:

ORA-08689: CREATE ASSERTION failed
ORA-08697: SYS owned tables are not supported.

I’m not using SYS, and cannot use SYS because I’m on the managed service “Autonomous Database” which doesn’t give SYSDBA privileges.

I reproduced this in a local environment to trace the SQL statements (ALTER SESSION SET EVENTS 'sql_trace bind=false, wait=false') and found that Oracle internally checked this constraint with:

/* SQL Analyze(250,0) */
SELECT /*+ ALL_ROWS BYPASS_RECURSIVE_CHECK */
1 FROM "SYS"."DUAL" WHERE (
  NOT EXISTS (
    SELECT 1
    FROM emp_salary s
    JOIN emp_commission c ON s.empno = c.empno
    WHERE s.salary + NVL(c.commission, 0) > 1000 + 100 + 42
  )
)

The good old DUAL table is owned by SYS and cannot be used in the assertion.

After trying many variations, I realized that SQL assertions are always evaluated with a SELECT from “SYS”.”DUAL”, even for cases that work, like in my previous post on SQL Assertions.

Then, I remembered what I’d learned from 30 years of working with Oracle Databases: if something fails with an ANSI join in Oracle, try the legacy join syntax (no JOIN clauses, only WHERE conditions). When ANSI joins were added in 9i, Oracle implemented them with internal transformations that affected other features. Most of those issues were fixed over the 20 years that followed, but SQL assertions seem to have fallen into that trap.

With the legacy join syntax, the internal query still reads “SYS”.”DUAL”, but it works:

=====================
PARSING IN CURSOR #140737366953320 len=208 dep=0 uid=136 oct=290 lid=136 tim=6202261308 hv=3501011950 ad='7ffff8be4960'
 sqlid='fzcfcfv8auczf'
CREATE ASSERTION salary_plus_commission_le_1150
CHECK (
  NOT EXISTS (
    SELECT 'violation'
    FROM emp_salary s, emp_commission c
    WHERE s.empno = c.empno
      AND s.salary + c.commission > 1150
  )
)
END OF STMT

...

=====================
PARSING IN CURSOR #140737364327008 len=286 dep=1 uid=136 oct=3 lid=0 tim=6202334328 hv=225567679 ad='783b9f10' sqlid='fv1va6n6r3sxz'
 /* SQL_ASSERTION obj#=72600 */ SELECT /*+  ALL_ROWS BYPASS_RECURSIVE_CHECK */ 1 FROM "SYS"."DUAL" WHERE (   NOT EXISTS (SELECT 'violation' "'VIOLATION'" FROM "FRANCK"."EMP_SALARY" "S","FRANCK"."EMP_COMMISSION" "C" WHERE "S"."EMPNO"="C"."EMPNO" AND "S"."SALARY"+"C"."COMMISSION">1150) )
END OF STMT

Two things to remember:

  • SQL Assertions provide correct consistency boundaries to avoid write skew in the absence of a serializable isolation level.
  • ANSI joins are nice, but Oracle was designed before this standard, with another syntax, and some bugs persist. Falling back to the old syntax may be safer.

Engineering Is a Team Sport. Your Incentives Aren’t.

I have a game tape dashboard. It tracks constraints and throughput across my engineering team … PR cycle times, review patterns, where work slows down and where it moves. I built it because I believe engineering is a team sport, and team sports need game film. You can’t coach what you can’t see.

A few years into leading my current team, I opened that dashboard and saw something I couldn’t ignore. Pull requests with over a thousand lines of code were taking, on average, five minutes longer to review than PRs with two hundred and fifty to five hundred lines. Five minutes. For four times the code.

The math told the story before anyone had to say a word out loud. PRs were sitting for days. When reviews did happen, they were almost always handled by the same one or two people. Everyone else had their own features to ship.

The math told the story before anyone had to say a word out loud.

The System I Inherited

I hadn’t built this team from scratch. I walked into an incentive structure that had been in place long enough to become invisible … one of those systems that nobody designed on purpose and nobody questions because it’s just always been there.

Engineers were measured by ticket throughput. How many you closed. How fast you moved from one to the next. Whoever did the most tickets won. That was the implicit scoreboard, even if nobody said it in a meeting or wrote it in a doc.

It’s a seductive way to measure a team when you’re under delivery pressure. Tickets closed feels like progress. Velocity trending upward feels like health. And for a while, it is. But a system designed to maximize individual throughput produces exactly one thing when applied to work at the team level like code review … engineers who are very good at protecting their time from other people’s problems.

When someone else’s PR sits in the queue, that’s not your problem. Your problem is the ticket in front of you. The one after that. The sprint goal attached to your name. Someone else being blocked is unfortunate, but it doesn’t change your scoreboard. So you keep moving.

Earlier in my career I was the manager who pulled up a velocity chart in planning and pushed for the number to go higher. The team complied. Velocity climbed. Leadership was happy. And quietly, underneath all of it, the codebase was accumulating debt that didn’t show up until something broke. I set the expectation and got exactly what I asked for.

Incentives work. Just not always in the direction you intended.

Why Every Process Fix Misses

The standard playbook for slow PR reviews follows a familiar script. Break your work into smaller PRs. Create better review templates. Set up a rotation schedule so the same two people aren’t carrying the load. Add SLAs with automated reminders. Some of these things have limited value. None of them address the actual problem.

If your performance is still measured by what you ship individually, a review rotation schedule just means it’s your turn to pull time away from shipping. The calendar entry exists. The incentive hasn’t moved. So the review gets done … technically. But it gets done the way you’d expect. Skim. Approve. Move on. The queue clears and the same pattern comes back within a quarter.

When you treat an incentive problem as a process problem, you get process-shaped results. The symptom quiets down. The cause stays exactly where it was.

What Actually Changed

My first instinct when I saw that dashboard data was to reach for a new metric. Measure review participation alongside ticket throughput. Track who was reviewing, how often, how quickly. Put the numbers somewhere visible.

The problem is that metrics can be bluffed. Give people a number to hit and they will find the path of least resistance. If review participation becomes a tracked metric, you’ll get reviews. You won’t necessarily get reviews that actually catch anything. You’ll get the five-minute rubber stamp dressed up as engagement, and your dashboard will look healthy while quality stays exactly where it was.

So instead of a new metric, we built a behavior.

When an engineer opens a PR, they drop it in a dedicated channel. The team sees it. Not as an item on a rotation list or an obligation attached to a schedule, but as a ball in play. The way I described it to the team was recovering a fumble in football. Somebody put something valuable on the field. The whole team swarms.

What made it work wasn’t the channel. It was what the channel represented … a shift from “your work is yours until it merges” to “the work belongs to all of us until it ships.” Reviewing a teammate’s PR wasn’t taking time away from your sprint anymore. It was part of your sprint. Getting someone unblocked counted the same as closing your own ticket, because the thing we were actually measuring was how fast the team moved work through the system.

The senior engineers who had been too busy to review suddenly had time. Because now someone else being blocked was their problem too.

We went from review cycles that ran three days to same day.

We went from review cycles that ran three days to same day.

The Number That Actually Matters

I come back to the game tape dashboard here. The metric I actually care about is team throughput, not individual velocity. How fast does work move from opened to merged to deployed, across the whole team, over a rolling period of time.

Individual velocity can look healthy while team throughput quietly collapses. One engineer closing six tickets while four of their teammates’ PRs sit unreviewed is not a win. It’s a transfer. You moved the constraint from one column to another and called it productivity.

The fastest teams I’ve built had one thing in common. Helping a teammate ship counted as shipping. Not rhetorically. Not in the values doc. In actual performance conversations, in how growth was talked about, in what got recognized out loud.

That’s where the behavior change lives. Not in the channel, not in the dashboard, not in any rotation schedule. In what your engineers believe will happen in their next performance conversation if they spend an afternoon doing thorough, careful reviews instead of closing their own tickets.

If the honest answer is “nothing good” … you already know what to fix.

It’s not the process.

One email a week from The Builder’s Leader. The frameworks, the blind spots, and the conversations most leaders avoid. Subscribe for free.