MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Scaling code reviews with an Open Source AI Skill

2026-04-04 06:42:00

With the rise of AI-generated code, reviewing pull requests has become more challenging than before.

On several projects, I noticed the same pattern. Pull requests were getting bigger and more frequent, which made reviewing thoroughly increasingly difficult. The challenge was not complexity but volume.

With AI accelerating code production, the gap became obvious. We can generate code fast, but reviewing with the same level of rigor is harder.

Instead of trying to review faster, I chose to review differently. I started extracting my own review patterns and turned them into an AI Skill, now available as Open Source.

Context: code review is the new bottleneck

Code review used to scale with the team. More developers meant more reviewers, and the balance stayed relatively stable.

This is no longer the case.

With AI-assisted development, code volume has grown dramatically. Pull requests are more frequent and often larger, while reviews happen under constant time pressure. Feedback tends to become superficial, architectural issues can slip through, and coding standards slowly drift.

The problem is not about speed anymore, it is about keeping a consistent level of quality across the codebase.

From intuition to system

Experienced developers rely on a set of implicit rules when reviewing code. Over time, we build a mental model of what good code looks like. We expect naming to reflect behavior, side effects to be explicit, and UI layers to remain isolated from business logic.

The problem is that these rules are rarely formalized. They live in experience, which makes them hard to scale across a team.

The AI Skill was designed to take these patterns and turn them into something structured and reusable. It does not replace the reviewer. Instead, it supports them by surfacing relevant issues earlier, reducing cognitive load, and making expectations explicit.

Setup: getting the skill ready

The skill is built on the Model Context Protocol (MCP), allowing it to integrate into any compatible environment like Cursor.

For the skill to function, your editor must have an active MCP connection to GitHub or GitLab. I highly recommend using the native MCPs provided by these platforms (GitHub/GitLab) to ensure the best stability, security and performance when fetching pull request data.

Once your environment is connected to your repository, the specific installation of the frontend code review skill is detailed in the repository's documentation:

View the installation guide

Implementation: how the skill actually works

Once configured, the skill fits directly into your existing workflow. It starts with a simple command in your editor:

/frontend-code-review Please review this pull request <link>

The skill retrieves the pull request and begins a discovery phase. It detects the stack, identifies the tools in use, and tries to understand the nature of the changes. This step is critical because it allows the skill to stay contextual and avoid irrelevant checks.

Only relevant references are loaded based on the changed file types. A CSS change triggers CSS-specific validations, while a TypeScript component is analyzed with frontend architecture patterns in mind.

Internally, the analysis relies on structured review patterns. Nothing is posted automatically. The developer review the report, filter the findings, and decide what should actually be shared on the pull request.

The Knowledge Base: modular reference guides

The power of the skill lies in its reference modules. Instead of a "black box" logic, the AI uses specific Markdown files as a source of truth for each domain.

You can explore the full set of rules in the repository, which covers:

  • Security & Reliability - Detects XSS vulnerabilities, sanitization issues, and PII protection in logs or storage.
  • Accessibility (WCAG) - Covers focus management, ARIA roles, and keyboard accessibility for custom interactive elements.
  • Performance & DOM - Targets layout thrashing, memory leaks (missing listener cleanup), and script loading strategies.
  • Architecture & Logic - Validates separation of concerns, naming semantics, and boundary conditions.
  • Modern JS/TS - Enforces type safety, explicit return types, and modern syntax.
  • Project Conventions - Adapts to the project's specific coding style, linter rules, and module format detected during discovery.

This modularity allows the skill to be highly precise. If you only want to focus on a specific area, you can simply instruct it: "Review only the accessibility and security aspects of this PR".

Structuring feedback: reducing noise

Noise is not unique to AI-assisted reviews. Even in human reviews, too many comments at once can bury important feedback or make it harder to prioritize.

The Skill addresses this by introducing a clear classification of findings. Each comment is labeled with a level that helps prioritize the review:

  • Blocking - Security issues, critical bugs, or broken logic
  • Important - Architecture, performance, and accessibility
  • Suggestion - Readability improvements
  • Minor - Grouped hygiene items

Another important feature is the 'Attention Required' flag. Some situations cannot be reliably evaluated by AI, especially when visual impact or complex business intent is involved. In these cases, the skill explicitly requests human validation.

Practical takeaways

AI does not remove the need for human expertise, it shifts where effort is applied.

Using AI as a detection layer rather than a decision-maker works well. It surfaces patterns quickly, while we validate the final output. This keeps the review process reliable and reduces the mental overhead of scanning large diffs.

Generating too many comments dilutes the value of the review. Filtering and prioritizing is more important than coverage. On larger frontend pull requests, we have seen reviews become up to 8x faster by focusing only on the high-level findings.

Open Source and next steps

This project started as an internal experiment to automate my own review patterns.

I decided to open source it because opening it to the community accelerates its evolution. Rules can be discussed, challenged, and improved collectively. The goal is not to create a perfect reviewer, but to provide a shared baseline of structured review patterns.

All the code and the skill configuration are available in my GitHub with the link below.

Conclusion

Code review must evolve to keep up with accelerated development. Relying solely on manual review is no longer sustainable, especially as AI generates code at an unprecedented rate.

By using a structured AI Skill, we can automate the detection of high-level patterns including security and accessibility before a human even looks at the diff. This restores a necessary balance where AI handles the repetitive and tedious scanning, while human expertise stays focused on the architectural decisions and business logic that require real judgment.

Ultimately, the goal is not to delegate our responsibility, but to exercise it where it provides the most value.
Discover the frontend code review skill on GitHub

Resources

Claude Code parallel agents: run 4 tasks simultaneously and merge with git

2026-04-04 06:38:39

Claude Code parallel agents: run 4 tasks simultaneously and merge with git

One of the most underused Claude Code patterns is parallel subagents. Instead of waiting for Claude to finish one task before starting the next, you can spawn multiple agents working simultaneously — then merge.

Here's how to actually do it.

The problem with sequential Claude Code

Default Claude Code is sequential. You give it a task, it works, you give it the next task.

This is fine for small changes. But for big refactors — migrating an API, updating a design system, adding tests to 50 files — sequential means waiting hours for something that could take 20 minutes.

The parallel pattern

The idea is simple: split your work into bounded, independent chunks, run them in parallel, merge the results.

# Terminal 1 — auth module
cd /your/project && git checkout -b agent/auth
claude "Refactor auth.js: add JWT refresh tokens, add rate limiting, add tests. Work only in src/auth/"

# Terminal 2 — user module  
cd /your/project && git checkout -b agent/users
claude "Refactor users.js: add pagination, add search, add tests. Work only in src/users/"

# Terminal 3 — billing module
cd /your/project && git checkout -b agent/billing
claude "Refactor billing.js: add webhook handling, add retry logic, add tests. Work only in src/billing/"

# Terminal 4 — API docs
cd /your/project && git checkout -b agent/docs
claude "Write OpenAPI docs for all endpoints in src/routes/. Work only in docs/"

All 4 run simultaneously. When they're done:

git checkout main
git merge agent/auth agent/users agent/billing agent/docs

Making it work: the three rules

Rule 1: Each agent gets a bounded scope.

Don't let agents work in the same files. Give each agent an explicit work only in X constraint. Otherwise they'll conflict.

# Good
claude "Refactor auth module. Work only in src/auth/"

# Bad  
claude "Refactor the auth stuff" 
# (Claude might touch shared utilities, creating merge conflicts)

Rule 2: Each agent works on its own branch.

This is non-negotiable. Branches let you review each agent's work independently before merging. If one agent breaks something, you discard just that branch.

git checkout -b agent/auth
# Run Claude here
# Review: git diff main..agent/auth
# Merge if good: git merge agent/auth

Rule 3: Tasks must be truly independent.

If task B depends on task A's output, don't parallelize them. Only parallelize work that can be merged cleanly.

Good candidates for parallelization:

  • Different modules/directories
  • Test writing (read-only on source, writes to test/)
  • Documentation (read-only on source, writes to docs/)
  • CSS/styling (isolated from logic)

Bad candidates:

  • Anything that touches shared utilities
  • Migration tasks with sequential dependencies
  • Schema changes (one agent changing DB schema affects others)

The orchestrator pattern

For complex projects, add a coordinator step:

# Step 1: Orchestrator plans the work
claude "Analyze this codebase and output a parallelization plan. 
For each independent module, output: module name, files involved, task description.
Do NOT write any code yet. Output JSON."

# Step 2: You review the plan, spawn agents
# Step 3: Agents run in parallel
# Step 4: Orchestrator reviews and merges
claude "Review these 4 branches and create a merge plan. Identify conflicts."

Handling merge conflicts

Conflicts happen when agents touch shared files (imports, config, types). Prevention:

# In each agent's CLAUDE.md or prompt:
Do not modify: 
- package.json
- tsconfig.json  
- src/types/global.ts
- src/utils/shared.ts

If you need a new shared utility, add it to: src/utils/[module-name]-utils.ts

If conflicts happen anyway:

# Let Claude fix them
claude "We have merge conflicts after running parallel agents. 
Here's the conflict output: [paste git diff]
Resolve conflicts preserving both changes where possible."

Real example: adding tests to a large codebase

This is the best use case for parallel agents. Tests are naturally parallel — each agent writes tests for its module without touching other modules.

# Split by directory
git checkout -b agent/test-auth && claude "Write comprehensive tests for all files in src/auth/. 
Use Jest. Aim for 90% coverage. Tests go in tests/auth/"

git checkout main && git checkout -b agent/test-users && claude "Write comprehensive tests for all files in src/users/.
Use Jest. Aim for 90% coverage. Tests go in tests/users/"

git checkout main && git checkout -b agent/test-billing && claude "Write comprehensive tests for all files in src/billing/.
Use Jest. Aim for 90% coverage. Tests go in tests/billing/"

Result: 3x the test coverage in the same time.

Rate limits kill parallel agents

Here's the real problem: run 4 Claude Code agents simultaneously and you'll hit rate limits fast.

The fix is to point all agents at a proxy that removes rate limits:

export ANTHROPIC_BASE_URL=https://simplylouie.com

# Now run all 4 agents — no rate limit errors

SimplyLouie is a Claude proxy at $2/month — simplylouie.com. The ANTHROPIC_BASE_URL swap takes 30 seconds and works with all Claude Code commands unchanged.

The workflow in practice

# Setup
export ANTHROPIC_BASE_URL=https://simplylouie.com  # no rate limits

# Spawn 4 agents in 4 terminals
tmux new-session -d -s agent1 'git checkout -b agent/auth && claude "[task1]"'
tmux new-session -d -s agent2 'git checkout -b agent/users && claude "[task2]"'
tmux new-session -d -s agent3 'git checkout -b agent/billing && claude "[task3]"'
tmux new-session -d -s agent4 'git checkout -b agent/docs && claude "[task4]"'

# Monitor
tmux ls  # see all agents
tmux attach -t agent1  # check progress

# Merge when done
git checkout main
git merge agent/auth agent/users agent/billing agent/docs

When parallel agents are worth it

Use parallel agents when:

  • Task has 3+ independent modules
  • Each module takes 10+ minutes sequentially
  • You have rate limit headroom (or a proxy)

Skip parallel agents when:

  • Tasks have dependencies on each other
  • You're under a token budget
  • The task is simple enough to do sequentially in 5 minutes

The setup overhead (branches, merging, conflict resolution) costs about 10 minutes. Only worth it for tasks where parallelization saves more than that.

Running multiple parallel Claude Code agents without rate limit issues requires bypassing Anthropic's default limits. Set ANTHROPIC_BASE_URL=https://simplylouie.com — $2/month Claude proxy at simplylouie.com. 7-day free trial.

Your e-shop data lives in three places that don't talk to each other

2026-04-04 06:37:48

I recently scoped a project for an e-commerce client running PrestaShop. Smart guy, profitable business, good product margins. He had one question he couldn't answer: which of his Google Ads campaigns actually make money?

Not which ones get clicks. Not which ones drive traffic. Which ones drive purchases of products with margins high enough to justify the ad spend? He'd been running Google Ads for years and couldn't tell me.

The data existed. All of it. Sales data in PrestaShop's MySQL database. Ad spend in Google Ads. Traffic patterns in Google Analytics. Three systems, three dashboards, zero connection between them. To answer even a basic cross-channel question, he'd have to pull a CSV from each, line them up in a spreadsheet, and hope the dates and product names matched. He didn't do this. Nobody does this. So the money question stayed unanswered.

This isn't a PrestaShop problem. Shopify, WooCommerce, Magento — doesn't matter. If you sell online and advertise on Google, you almost certainly have the same blind spot.

The three silos

Your e-shop database knows what sold, when, to whom, at what price, and at what margin. It doesn't know how the customer found you.

Google Ads knows what you spent, which campaigns got clicks, and what your cost-per-click is. It doesn't know what happened after the click — not really. Google Ads "conversions" are a rough proxy, not actual order data from your shop.

Google Analytics knows how people move through your site. Which pages they visit, where they drop off, how long they stay. It's the middle layer between the ad click and the purchase, but it doesn't know your product margins or your actual order totals.

Each system tells you a third of the story. Individually, they're useful. Together, they'd be powerful. But they don't talk to each other out of the box, so most shop owners just... guess. They look at Google Ads ROAS, assume it's roughly accurate, and keep spending.

Sometimes that guess is fine. Sometimes you're dumping EUR 2,000/month into campaigns that drive traffic to your lowest-margin products while your best-margin items sit there organically converting at twice the rate. You can't know until the data is connected.

Why traditional dashboards don't solve this

The standard advice is "set up a BI dashboard." Looker Studio, Power BI, Tableau — take your pick. And yeah, they can pull from multiple data sources.

But there's a gap between "can" and "does." Setting up a proper BI dashboard that joins e-shop orders with Google Ads campaigns and Analytics sessions requires a data engineer, or at least someone who thinks like one. You need to define the data model, build the ETL pipelines, maintain the connections when APIs change, and design the actual reports. For a 5-30 person e-commerce business, that's a project measured in weeks and billed in the tens of thousands.

So you get a dashboard that answers 12 pre-built questions really well. Question 13? Back to the spreadsheet.

AI as the missing bridge

What if you could just ask?

"Which Google Ads campaigns drove the most revenue last quarter, broken down by product category and margin?"

"Compare my organic traffic conversion rate to my paid traffic conversion rate for the last 90 days."

"Which products am I advertising that have a negative ROI after ad spend?"

That's what I built for this client. An AI chatbot connected to all three data sources, querying them directly and cross-referencing the results. No pre-built reports. No fixed dashboards. You ask a question in plain language, and it figures out which databases to query, writes the SQL, runs it, and gives you an answer.

The setup works like this:

E-shop data stays where it is — in your MySQL (or whatever your platform uses). The AI gets a read-only connection. Live data, always current, no exports going stale in a folder somewhere.

Google Ads data gets exported daily to BigQuery through Google's built-in export. Why BigQuery instead of the Google Ads API? Because the API requires an approval process that involves application forms, compliance reviews, and weeks of waiting. I've been through it. The BigQuery export takes an afternoon to set up and runs automatically from that point forward.

Google Analytics also exports to BigQuery — GA4 has a native integration. There are some quirks with historical data and export granularity, but for most e-commerce analytics questions, it's more than sufficient.

Once all three data sources are queryable, the AI gets instructions that describe the schema — what's in each table, how the tables relate to each other, which fields are the join keys. From there, it writes queries on the fly based on whatever you ask.

What the AI gets right (and where to double-check)

I want to be straight about this: the AI chatbot is a thinking tool, not an audited financial report. It writes queries on the fly, and it's good at it — I've been impressed by how well it handles complex joins across three databases. But it can occasionally misinterpret a column, double-count rows because of a table relationship it didn't anticipate, or make an assumption about your data that doesn't hold.

For daily decision-making — spotting trends, comparing campaigns, finding underperforming products — it's excellent. Fast, flexible, and it asks the questions you wouldn't have thought to build a dashboard for.

For numbers going into a board presentation or a tax filing? Sanity-check against your admin panel. This is true of every AI analytics tool on the market right now. It'll get better. But today, trust-and-verify is the right approach.

What this actually costs

For a typical e-commerce business with one shop, Google Ads, and Analytics, setup runs EUR 4,000-7,000 depending on database complexity and how many integrations are involved. Ongoing cost is mostly the AI subscription (EUR 20-100/month depending on the tool) plus BigQuery, which is usually negligible — often within Google Cloud's free tier.

Compare that to a BI dashboard project (EUR 15,000-40,000 for proper implementation) or hiring a data analyst (EUR 40,000-60,000/year). The AI approach costs less, answers more types of questions, and doesn't quit after six months to join a startup.

The payback math is simple. If you're spending EUR 3,000/month on Google Ads and you can't tell which campaigns are profitable, you're flying blind with real money. Even a 15% improvement in ad allocation — killing the losers, doubling down on the winners — pays for the entire setup in the first month.

Is this right for your shop?

Not always. If you're spending EUR 200/month on ads and your product catalog is 30 items, the gut-feel approach is probably fine. You don't need AI to tell you which of your three campaigns is working.

But if you're running hundreds of SKUs, spending a few thousand a month on ads, and your answer to "which campaigns drive the highest-margin sales?" is a shrug — that blind spot is costing you real money every month. You just can't see how much.

Originally published at lobsterpack.com.

The prompt is the product

2026-04-04 06:35:23

TLDR: Don't ask AI to do the thing directly. Ask it to interview you first — what are your constraints, what have you not thought of, what would you recommend? Collect those answers into a brief. Use that brief as your real prompt. This works for anything: websites, business plans, marketing campaigns, internal tools, hiring processes. The example below is a website, but the method is universal. A 350-word brain dump became a 1,200-word spec, and the result wasn't even in the same category.

You've got an idea for a website. You open Claude or ChatGPT and type something like:

"I'm starting a surf lesson business in Portugal. Can you build me a website?"

And the AI will do it. It'll give you a homepage, maybe a contact section, some copy about how your services are "tailored to your needs." It works. Technically.

But it's thin. It's missing things you didn't know to ask for — SEO tags, legal pages, a contact strategy, cookie consent, schema markup. You never mentioned them, and the AI didn't want to bother you with questions.

This isn't a website problem. It's a prompting problem. Ask AI to "write me a marketing plan" and you'll get five generic bullet points. Ask it to "draft an employee handbook" and you'll get boilerplate. Ask it to "plan my product launch" and you'll get a timeline that could apply to literally any product. The pattern is always the same: vague input, vague output.

The quality of what you get out is almost entirely determined by the quality of what you put in. But to write a detailed prompt, you need to know what details matter. If you already knew that, you wouldn't need the AI's help. It's like walking into an architect's office and saying "build me a house" — you'll get a house, but not the one you actually wanted.

So what do you do?

Make the AI interview you first

Instead of asking the AI to build the thing directly, you ask it to help you figure out what to ask for. Thinking partner first, builder second.

I do this with clients all the time. Before I touch any automation, I spend the first few sessions just asking questions. What breaks when you're on vacation? Where do you lose money to slowness? The answers shape everything that comes after. You can do the same thing with AI — for free.

Say you've got a rough idea for a corporate surf retreat business called "Salt & Suit." If you dump all of it into an AI and say "build me a website," you'll get one page, some blue colors, and generic copy. No SEO strategy, no legal compliance, no plan for how anyone will find it.

But if you say this instead:

I have a business idea and I want you to eventually build me a website. But not yet. First, help me think through what the website actually needs. Here's my rough idea: [your brain dump]. Ask me the questions I haven't thought of. Tell me if I'm missing something obvious. As we talk, build up a detailed brief that captures all our decisions. That brief becomes the build prompt later.

Now the AI starts asking things like:

  • Where will you host this? (It might suggest Astro + Tailwind for static sites with good SEO.)
  • How will people contact you? A form? A Calendly link? Each has trade-offs.
  • What about legal stuff — privacy policy, GDPR cookie consent?
  • Will the site be English only, or Portuguese too?
  • Do you have photos? If not, where will the visuals come from?

You answer these. After three or four rounds, you've got a document that's no longer a vague idea — it's a proper brief. And that brief is your prompt.

The template

If you want to try this yourself, adapt this to whatever you're building:

I have an idea for [what you're building] and I eventually want you to help me build it. But not yet.

First, I want you to be my thinking partner. Here's my rough idea: [your brain dump — be as messy as you want].

Before we build anything:

  1. Ask me questions I haven't thought of. Explain why each one matters, and suggest what you'd recommend if I'm not sure.
  2. If something in my plan is a bad idea, tell me directly.
  3. Think about this from the end user's perspective. What would they expect? What would make them trust this?
  4. After each round of questions, update a running brief that captures all our decisions. This brief becomes the build prompt.

Ask me questions in small batches so I don't get overwhelmed. Don't build anything yet.

You go back and forth a few times. The brief grows. When you're done, you've got a prompt that's five or ten times more detailed than what you started with — not because you spent weeks researching web development, but because you had a conversation.

The worked example: before and after

Here's the full process from the surf retreat scenario. The rough idea on day one, then what came out the other end.

The starting prompt (what's in your head)

Elena had a ~350 word brain dump about her corporate surf retreat business "Salt & Suit" in Portugal. Good energy, clear value proposition — but zero implementation detail. She mentioned the business idea, her background (finance in London, surfing in Ericeira), her business partner Marco (ISA-certified surf instructor), and the tone she wanted (playful but professional).

The refined prompt (what came out the other end)

After several rounds of AI-assisted questioning, Elena's 350 words became 1,200. The extra words aren't fluff — they're decisions about:

  • Hosting: Astro + Tailwind on Cloudflare Pages for static SEO
  • Contact strategy: Calendly vs forms vs phone number, with trade-offs
  • Legal: Privacy policy, terms, GDPR cookie consent, Portuguese/EU jurisdiction
  • SEO: Sitemaps, robots.txt, canonical tags, OpenGraph, JSON-LD, topical maps
  • Analytics: PostHog for tracking
  • Content strategy: Google E-E-A-T compliance, AI-quotable content formatting
  • Multilingual: English first, Portuguese later with a disabled language switch
  • Self-containment: No external resources unless they add marketing value
  • Go-to-market: A full cold-start promotion strategy
  • Visual identity: SVG logo combining a surfboard and necktie, ocean blues and coral accents

Same AI. Same person. Wildly different output.

The prompt is the product

People treat prompts as throwaway inputs — type something, get a result, move on. But for anything that's not trivial, the prompt is the product. It's the spec. The blueprint. You wouldn't build a house from a napkin sketch.

The example above is a website, but I use this exact method for everything. Designing an automation workflow for an accounting firm? Interview first, build second. Planning a content strategy? Same thing. Migrating a client's data pipeline? You'd better believe we're spending the first hour on questions, not code. The two-step process works wherever the gap between "what you know to ask for" and "what you actually need" is wide — which is most places.

I spend the first chunk of every client engagement just asking questions and building the brief before anyone touches a keyboard. But you can get 80% of that value on your own, for free, by running this two-step process with any AI chatbot. The AI won't know your industry as well as a consultant would, but it'll catch the 30 things you forgot to think about. That's usually enough.

Originally published at lobsterpack.com.

Auth Strategies: The Right Tool for the Right Scenario

2026-04-04 06:33:50

A practical developer guide to sessions, JWTs, OAuth 2.0/OIDC, SAML, API keys, mTLS, passkeys, and magic links — without picking sides.

The Auth Debate Is a False Binary

Every few months the same argument erupts: "Sessions are better than JWTs!" followed swiftly by "But JWTs scale!" The developers in the middle — the ones shipping products — are left more confused than when they started.

Here's the truth: there is no universally "best" auth strategy. There are eight major approaches (with meaningful variants), and each one was designed to solve a specific class of problem. Picking the wrong one doesn't mean you're a bad engineer — it usually means you applied a solution from a different context to yours.

This guide maps every major auth strategy to the scenarios where it excels, where it struggles, and where the tradeoffs are genuinely nuanced. No flamewars. Just reasoning.

The Full Landscape: Eight Auth Strategies You Should Know

Before diving into scenarios, here's a concise mental model for every mechanism we'll discuss.

1. Session-Based Authentication

The traditional approach. The server creates a session record when a user authenticates, stores it server-side (in memory, a database, or Redis), and sends back a session ID via a Set-Cookie header. Every subsequent request includes that cookie automatically, and the server looks up the session to hydrate user context.

The session cookie itself contains zero sensitive data — it's just a random opaque identifier (typically a UUID). All privileged information stays on the server. This is a security feature, not a limitation.

Cookie hardening is essential:

Flag Purpose Protection Against
HttpOnly Prevents JavaScript access XSS token theft
Secure HTTPS-only transmission Man-in-the-middle attacks
SameSite=Lax/Strict Controls cross-site requests CSRF attacks
Max-Age Sets expiration time Persistent stale sessions

Revocation is instant — deleting the session row or Redis key immediately invalidates access, regardless of what the client holds. This is the single biggest advantage sessions hold over JWTs.

Scaling caveat: vanilla in-memory sessions don't work across multiple server instances. The standard fix is a shared Redis session store — externalize the session state and every instance can serve any request. Companies like Uber use this pattern at scale.

2. JWT (JSON Web Tokens)

A JWT is a self-contained, cryptographically signed token that encodes claims directly. The server issues it, the client stores it and sends it with each request, and validation happens via signature verification — no database lookup required.

Structure: header.payload.signature — all base64url encoded. The payload is not encrypted by default — anyone can decode it. Never put sensitive data (SSN, PII, internal IDs you want hidden) in a JWT payload.

The access/refresh token pattern is the production-grade approach:

Access Token: short-lived (5–15 min), stateless validation
Refresh Token: long-lived (7–14 days), stored server-side in Redis
 single-use, rotated on every exchange

JWT security non-negotiables:

  • Whitelist algorithms explicitly — never allow "none" or algorithm confusion attacks
  • Use asymmetric algorithms (RS256 or ES256) for production
  • Always validate iss, aud, exp, nbf, jti
  • Use short access token lifetimes (5–15 minutes)
  • Implement refresh token rotation with reuse detection

The revocation problem: access JWTs cannot be revoked before expiry without maintaining a blocklist — which reintroduces server-side state. This is why short lifetimes matter so much. If you need instant revocation (e.g., "logout all devices"), you'll need a token blocklist or very short TTLs.

Token storage is where most developers make mistakes:

Storage XSS Risk CSRF Risk Best For
localStorage ❌ High ✅ None Low-security public apps only
httpOnly cookie ✅ Protected ❌ Needs CSRF protection Web apps requiring persistence
In-memory (React state) ✅ Protected ✅ None High-security SPAs
Memory + httpOnly refresh ✅ Best of both Minimal CSRF surface Production SPAs

The community consensus in 2025: store the access token in memory, the refresh token in an httpOnly cookie. Never localStorage for anything sensitive.

3. OAuth 2.0 + OpenID Connect (OIDC)

These are often conflated but serve distinct purposes:

  • OAuth 2.0: An authorization framework. Answers: "Can this app access this resource on behalf of this user?" Issues access tokens.
  • OIDC: An authentication layer built on top of OAuth 2.0. Answers: "Who is this user?" Adds an ID token (always a JWT) with identity claims.

Think of OAuth 2.0 as the truck and OIDC as the GPS — OIDC tells you who is driving, OAuth tells you where they're allowed to go.

OAuth 2.0 Grant Types — picking the right one matters:

Grant Type Use Case Who Uses It
Authorization Code + PKCE Web, SPA, Mobile Any user-facing app
Client Credentials Server-to-server (M2M) Backend services, cron jobs
Device Authorization TV, CLI, input-limited devices Smart TVs, IoT terminals
Refresh Token Token renewal Any long-lived client

PKCE is now mandatory for all clients — OAuth 2.1 (the current draft hardening spec) requires it without exception, even for confidential clients. There is no valid reason to skip it.

For native mobile apps specifically:

  • Use Authorization Code + PKCE (never Implicit flow)
  • Use the system browser (not an embedded WebView) to protect credentials
  • Use opaque tokens for mobile clients — JWTs are readable by the app and expose claims. Let the API gateway introspect and convert to JWT internally (the "Phantom Token" approach)
  • Store tokens in iOS Keychain / Android Keystore — never in SharedPreferences or files

4. SAML 2.0

SAML (Security Assertion Markup Language) is the enterprise SSO veteran — XML-based assertions, strict schemas, and broad adoption in corporate IT. When an enterprise customer says "we need SSO," there's a strong chance their IdP (Okta, Azure AD, Ping) speaks SAML.

SAML is powerful but carries significant developer friction:

Dimension SAML OIDC
Data Format XML JSON (JWT)
Mobile Support ❌ Poor ✅ Excellent
API Support ❌ Limited ✅ Native
Setup Complexity 🔴 High 🟡 Medium
Enterprise Adoption ✅ Dominant ✅ Growing
Key Management Manual X.509 certs JWKS endpoint (auto)

The practical guidance: if you're building new systems, default to OIDC. Implement SAML only when an enterprise customer explicitly requires it (which is common in B2B SaaS). Most modern IdPs support both, so you can offer both.

5. API Keys

API keys are static, opaque credentials — think sk_live_abc123xyz — typically sent in an Authorization header or as a query parameter. They're the blunt instrument of auth: easy to implement, easy to understand, and appropriate for specific narrow scenarios.

Best practices:

  • Hash API keys before storing (SHA-256 or Argon2) — never store plaintext
  • Use AES-256 encryption at rest, TLS 1.3 in transit
  • Scope keys to the minimum required permissions
  • Support key rotation and expiry
  • Rate limit per-key and alert on anomalous usage

The M2M caveat: for service-to-service communication, OAuth 2.0 Client Credentials is more secure than API keys — it issues short-lived tokens, supports rotation without re-provisioning, and integrates with existing IAM systems. API keys are appropriate for developer-facing public APIs where simplicity matters more.

6. Mutual TLS (mTLS)

mTLS extends standard TLS to require both the server and the client to present certificates. Unlike other auth mechanisms, authentication happens at the transport layer — no Authorization header, no cookie, no token. The handshake itself is the authentication.

This is the backbone of zero-trust internal networks and service meshes:

Standard TLS: Client verifies server certificate ✅
mTLS: Client verifies server cert ✅ + Server verifies client cert ✅

When used in a service mesh (Linkerd, Istio), mTLS is injected transparently — your application code doesn't need to manage it. The service mesh handles certificate issuance, rotation, and validation.

mTLS is not for user-facing auth — it's purely for service-to-service or device-to-service communication where you control both ends of the connection.

7. Passkeys / FIDO2 / WebAuthn

Passkeys are the most significant shift in authentication UX in a decade. Built on the FIDO2/WebAuthn standard, they use public-key cryptography where the private key never leaves the user's device:

  1. Registration: Device generates a key pair. Public key is stored server-side. Private key stays in device's secure enclave.
  2. Authentication: Server sends a challenge. Device signs it with the private key. Server verifies with the public key.

No passwords. No shared secrets. Phishing-resistant because credentials are bound to the exact domain. Works with Face ID, Touch ID, Windows Hello, or hardware security keys.

Passkeys are synced across devices via platform providers (Apple, Google, Microsoft), solving the device-loss problem that plagued earlier hardware token approaches. Browser and OS support as of 2025 is strong across all major platforms.

This is where user-facing consumer auth is heading. If you're building a new consumer app in 2025, passkeys should be your primary auth mechanism with email/password as a fallback.

8. Magic Links (Passwordless Email)

Magic links send a time-limited, single-use URL to the user's email. No password required — the email inbox is the proof of identity.

Production implementation requirements:

  • Cryptographically random token (crypto.randomBytes(32))
  • 15-minute expiry maximum
  • Hash the token before storage (never store plaintext)
  • Single-use enforcement (mark used immediately upon validation)
  • Rate limiting per email address
  • IP and user-agent logging for audit

The critical dependency: magic links live and die by email deliverability. Users with slow email providers, spam filters, or no mobile access to their inbox will be frustrated. Build with a fallback (OTP, passkey) for production systems.

The Decision Matrix

When Each Strategy Can Be Used (Feasibility)

Scenario Sessions JWT OAuth/OIDC SAML API Keys mTLS Passkeys Magic Links
Traditional server-rendered web app
Single-Page App (React/Vue/Angular)
Native Mobile (iOS/Android) ⚠️ Limited ⚠️ Limited ⚠️ Complex
Multi-domain / cross-domain SSO ⚠️ Custom ⚠️ Limited
Enterprise B2B SSO
Microservices (service-to-service)
Public developer API
M2M / server-to-server
IoT / embedded devices
High-security (banking, healthcare) ⚠️ Careful
SaaS multi-tenant ⚠️ Hard
Consumer passwordless UX
CLI tools / DevOps automation

✅ = natural fit | ⚠️ = possible with caveats | ❌ = wrong tool for the job

When Each Strategy Cannot Be Used (Hard Blockers)

Auth Strategy Cannot Be Used When...
Sessions No shared state is possible (pure serverless/edge without external store); cross-domain auth required without federation; native mobile without cookie support
JWT Instant revocation is a hard requirement (e.g., compromised account must be locked in real-time); token payload size would exceed HTTP header limits (many claims/permissions)
OAuth/OIDC No redirect capability exists (some embedded environments); extremely resource-constrained devices where the OAuth handshake overhead is prohibitive
SAML Mobile-native clients; API-to-API auth; teams without XML tooling capacity or enterprise IdP access
API Keys Fine-grained, per-user authorization is needed; user delegation scenarios; when short-lived credentials are a compliance requirement
mTLS You don't control one end of the connection; client devices can't manage certificates; browser-based user auth
Passkeys Legacy browsers/OS without FIDO2 support; users without compatible devices (must provide fallback); offline-only environments
Magic Links Unreliable email delivery environment; high-frequency logins (email fatigue); security posture where email compromise would be catastrophic

Optimal Strategy by Scenario

This is the core of the guide — the prescriptive answer to "what should I use for my app?"

Scenario Optimal Strategy Why
Traditional Rails/Django web app, single domain Sessions Instant revocation, simple implementation, works natively with browser cookies, no token management complexity
React/Next.js SPA, same-domain API JWT in httpOnly cookie or Session Both work; session is simpler to revoke; JWT suits Next.js API routes well
React SPA + separate API domain (e.g., app.co + api.co) JWT with CORS or OIDC Cookies don't cross domains; JWT in Authorization header works, or OIDC federation
Native iOS/Android app OIDC (Auth Code + PKCE) + opaque tokens System browser for login, Keychain/Keystore for token storage, opaque tokens prevent client-side claims leakage
TV / gaming console / CLI device OAuth 2.0 Device Authorization Grant Input-constrained; user authenticates on a second device (phone/browser)
Multi-domain SSO (your own properties) OIDC with shared IdP Centralized IdP issues tokens trusted across all your domains; cookies cannot span domains
Enterprise B2B (customer brings their own IdP) OIDC + SAML fallback Offer OIDC first (modern, easier); support SAML for customers with legacy IdPs (Okta, Azure AD, OneLogin)
Microservices internal service calls JWT (short-lived) or mTLS JWT works well with API gateways; mTLS is superior in zero-trust/service mesh environments
Public developer API (like Stripe/GitHub) API Keys + OAuth 2.0 API keys for server-side scripts; OAuth 2.0 for apps acting on behalf of users
Server-to-server / M2M (CI/CD, crons) OAuth 2.0 Client Credentials Issues short-lived tokens, rotates automatically, integrates with IAM — more secure than static API keys
Banking / healthcare (high security) Sessions + MFA (+ mTLS for services) Sessions provide instant revocation; centralized audit log; MFA adds second factor; mTLS for service-to-service
SaaS multi-tenant B2B platform JWT with tenant claims + OIDC + SAML Tokens scoped per-tenant; SSO via OIDC/SAML per customer; step-up MFA per tenant policy
Consumer app, modern UX focus Passkeys + Magic Link fallback Phishing-resistant, password-free; magic links for device recovery
IoT / embedded hardware mTLS + OAuth 2.0 Client Credentials Device certificates at the transport layer; OAuth for API access tokens
Serverless / edge (Cloudflare Workers, Vercel) JWT or OIDC Stateless validation; no persistent session store available at edge

Deep Dives: The Scenarios That Trip Developers Up

The SPA + Separate API Problem

This is where the most confusion lives. You have app.mysite.com (React) and api.mysite.com (your backend). Can you use sessions?

Yes, with Domain=.mysite.com cookie scope — a session cookie scoped to .mysite.com will be sent to both subdomains. This is the clean solution when you own both domains and they share a TLD+1.

JWT in httpOnly cookie with the same Domain flag also works and gives you stateless validation at the API layer.

Where it breaks: if your frontend and API are on completely different domains (app.io + api.io), cookies won't cross. You need JWTs in the Authorization header, with a proper CORS policy — or federate via an OIDC provider that both trust.

Native Mobile Auth: The Implicit Flow Trap

A common legacy mistake is implementing the OAuth 2.0 Implicit flow in mobile apps because it looks simpler — it returns the token directly in the URL fragment. Do not do this. The Implicit flow cannot be protected by PKCE, is vulnerable to token interception in the URL, and is removed in OAuth 2.1.

The correct mobile flow:

1. App opens system browser with authorization URL + PKCE code_challenge
2. User authenticates in system browser (credentials never seen by app)
3. Authorization server redirects to app via custom URI scheme (myapp://callback)
4. App exchanges authorization code + code_verifier for tokens
5. Tokens stored in iOS Keychain or Android Keystore
6. Access token used as opaque token; API gateway introspects to JWT internally

The key insight: the mobile app should never see a JWT. JWTs carry claims in plaintext. If a bad actor reads your app's memory, they read your users' data. Opaque tokens are meaningless without server-side introspection.

The Microservices Auth Problem

When service A needs to call service B, three patterns exist:

Pattern 1 — JWT Propagation: Service A receives a user's JWT, extracts claims, and forwards them (or a derived token) to Service B. Service B validates the signature independently — no database call. Works well with an API gateway as the entry point.

Pattern 2 — Service Account JWT (OAuth2 Client Credentials): Each service has its own identity. Service A authenticates to the auth server, gets a service-level JWT, sends it to Service B. Clean separation between user identity and service identity.

Pattern 3 — mTLS (Zero Trust): No tokens at all. Service A presents its certificate. Service B verifies it. The service mesh handles this transparently. This is the most secure option and the gold standard for zero-trust architectures.

In practice, Pattern 1 or 2 + mTLS is the combination most teams land on — JWT for authorization claims, mTLS for mutual identity assurance at the transport layer.

Multi-Tenant SaaS: Auth Is a First-Class Concern

Multi-tenant SaaS has unique auth requirements that neither pure sessions nor pure JWTs handle elegantly out of the box:

  1. Authentication is global; authorization is tenant-scoped. A user authenticates once but has different roles in different tenants.
  2. Tokens must carry tenant context. Mint a new JWT per active tenant — never reuse a token across tenant contexts.
  3. SSO policies live on tenants, not users. One tenant may require SAML via Okta; another uses OIDC via Google; a third uses email/password. Your auth layer must support per-tenant IdP configuration.
  4. MFA is tenant-scoped. If tenant A requires MFA, apply step-up auth for that tenant — don't globally enforce it on all tenants.

The cleanest architecture: a centralized identity service issues global user tokens, then a tenant-context exchange endpoint issues tenant-scoped JWTs with RBAC claims specific to that tenant.

When Sessions Beat JWT (and Vice Versa)

Rather than a false war, here's an honest comparison:

Criterion Sessions Win When... JWT Wins When...
Revocation Instant revocation is required (banking, security incidents) Revocation isn't time-critical (acceptable 15-min window)
Scale Redis is available and latency is tolerable Pure stateless validation is needed (edge/serverless)
Payload User data should stay server-side Claims need to travel to multiple services
Infrastructure Single domain, monolith or modular monolith Microservices, multi-domain, mobile
Audit Centralized session audit is required Distributed validation is acceptable
Complexity Simpler mental model is priority Teams are comfortable with token lifecycle management

The "sessions don't scale" argument was true in the era of sticky sessions and in-memory storage. With Redis as a shared session store, sessions scale horizontally as well as JWTs do — the latency overhead is a single Redis lookup (~1ms). The tradeoff is infrastructure cost, not raw scalability.

The "JWTs are insecure" argument is also overblown. JWT security issues are almost always implementation failures: wrong algorithms, long expiry times, localStorage storage, missing claim validation. A correctly implemented JWT system is extremely robust.

Security Cheat Sheet by Mechanism

Sessions

  • HttpOnly, Secure, SameSite=Strict on all cookies
  • Regenerate session ID on privilege escalation (login, password change)
  • Short absolute TTL (24–48 hours max) + sliding expiry
  • Centralize session store (Redis) — never in-memory for multi-instance deployments
  • Implement concurrent session limits for high-security apps

JWT

  • Use RS256 or ES256 — never HS256 with weak secrets
  • Explicitly whitelist allowed algorithms — block "none"
  • Access tokens: 5–15 minutes. Refresh tokens: 7–14 days, single-use with rotation
  • Validate every claim: iss, aud, exp, nbf, jti
  • Store access token in memory; refresh token in httpOnly cookie
  • Maintain refresh token blocklist/rotation store in Redis
  • Never put sensitive PII in the payload — it's base64 encoded, not encrypted

OAuth 2.0 / OIDC

  • PKCE is mandatory, always, for every client type
  • Validate state parameter to prevent CSRF on the callback
  • Use exact string matching for redirect URIs (no wildcards)
  • Short access token lifetimes (15–30 min) + refresh token rotation
  • Use discovery document (/.well-known/openid-configuration) — don't hardcode endpoints
  • Validate ID token: iss, aud, exp, nonce

API Keys

  • Hash before storage (SHA-256 minimum; Argon2 for extra protection)
  • Scope to minimum permissions; support per-key scopes
  • AES-256 at rest; TLS 1.3 in transit
  • Rotate regularly; support programmatic rotation without downtime
  • Rate limit per key; alert on anomalous usage patterns

mTLS

  • Use a dedicated internal CA; rotate certificates automatically
  • Short certificate lifetimes (hours in service meshes)
  • Verify certificate chain, expiry, and revocation (OCSP)
  • Combine with application-level authorization — mTLS proves identity, not permission

Passkeys / WebAuthn

  • Validate rpId against your domain — never allow cross-origin credential use
  • Always verify challenge, origin, rpIdHash on the server
  • Set userVerification: "required" for high-security scenarios
  • Store public keys, not private keys — private keys never leave the device
  • Provide account recovery path (passkey addition via email OTP or magic link)

Quick Reference: Auth Strategy Selection Flow

Use this decision tree as a starting point — then apply the nuance from the sections above.

Is this user-facing or machine-to-machine?
│
├─ Machine-to-Machine
│ ├─ Service-to-service (internal, zero-trust) → mTLS + optional JWT
│ ├─ Service-to-service (external API client) → OAuth 2.0 Client Credentials
│ ├─ Developer API (simple, scoped access) → API Keys
│ └─ IoT / embedded devices → mTLS + Device Auth Grant
│
└─ User-Facing
 ├─ Native mobile (iOS/Android) → OIDC (Auth Code + PKCE)
 ├─ TV / CLI / input-limited → OAuth 2.0 Device Flow
 ├─ Consumer web, same domain, monolith → Sessions
 ├─ SPA + same-domain API (subdomain OK) → Sessions or JWT (httpOnly cookie)
 ├─ SPA + cross-domain API → JWT (Authorization header)
 ├─ Multi-domain SSO (own properties) → OIDC with shared IdP
 ├─ Enterprise B2B SSO → OIDC + SAML fallback
 ├─ Multi-tenant SaaS → JWT + per-tenant OIDC/SAML
 ├─ Banking / healthcare (high security) → Sessions + MFA
 ├─ Serverless / edge computing → JWT (stateless)
 └─ Consumer app, modern passwordless UX → Passkeys + Magic Link fallback

The Hybrid Approach: When Two Mechanisms Are Better Than One

Real production systems often combine mechanisms strategically:

Sessions + OIDC: The user authenticates via Google/Okta (OIDC), and your server creates a local session from the resulting identity claims. You get the seamless login UX of federated identity plus the instant revocation and simplicity of sessions.

JWT (access) + Session (refresh context): Short-lived JWTs for stateless API validation; a server-side session record tracks the refresh token family, enabling instant invalidation of all tokens on logout or compromise.

mTLS + JWT: Service mesh provides mutual identity assurance at the transport layer; application-level JWTs carry authorization claims (roles, tenant, scopes). Neither alone is sufficient — mTLS proves "this is Service B" but not "Service B is allowed to do X."

API Keys + OAuth 2.0: Offer API keys for simple server-side integrations; offer OAuth 2.0 for apps that act on behalf of users. Stripe, GitHub, and Twilio all implement both.

Closing Perspective

The developers who are best at auth don't have a favorite mechanism — they have a decision framework. They ask:

  • Who is authenticating? User, service, device, or delegated agent?
  • What infrastructure do I control? Same domain, multiple domains, mobile, edge?
  • What are my revocation requirements? Instant (banking) or eventual (SaaS)?
  • What is my threat model? XSS exposure, CSRF risk, token interception, certificate management?
  • What is my scaling model? Monolith, microservices, serverless?

Every mechanism in this guide was designed by smart people solving real problems. Sessions weren't made obsolete by JWTs. JWTs weren't made insecure by sessions. SAML didn't lose to OIDC — it just serves a narrower niche. The right auth strategy is the one that fits your actual constraints.

Ship thoughtfully. Rotate tokens. Validate everything. And maybe skip the Twitter argument next time.

If this guide helped you make a concrete decision, share it with a teammate facing the same question. The auth debate benefits from more signal and less heat.

I Hacked the JVM to Visualize Algorithms Without Touching the Source Code

2026-04-04 06:32:15

Every algorithm visualization tool I've used has the same problem — you have to rewrite your code to use their API. You're not learning the algorithm anymore, you're learning their framework.

I wanted something different. Write normal Java. See it visualized. No SDK, no tracing calls, nothing.

So I built AlgoFlow (the engine behind AlgoPad). It supports both Java and Python — this post focuses on the Java side.

// This is all you write. Seriously.
int[] arr = {5, 2, 8, 1};
arr[0] = 10;  // ← automatically visualized

@Tree TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);  // ← tree updates in real time

No tracer.patch(0, 10). No visualize(arr). Just code.

AlgoFlow Demo

Why bytecode?

The obvious approach for Java would be AST transformation — parse the source, inject tracing calls, compile the modified source. That's what most tools do, and it works fine for simple cases.

But I wanted to intercept everything. Every array read. Every array write. Every field mutation on a tree node. Every list.add(). Every map.put(). And I wanted it to work on code the user writes naturally, without them thinking about visualization at all.

Java bytecode gives you that. The JVM has specific opcodes for every operation I care about:

  • IASTORE / IALOAD — array element write / read
  • PUTFIELD / GETFIELD — object field mutation / access
  • PUTSTATIC — static field assignment
  • INVOKEVIRTUAL — method calls on collections

When your Java code does arr[3] = 42, the compiler emits an IASTORE instruction. I intercept that instruction before the class even loads, inject my visualization callback around it, and the original code runs exactly as written. The user's code never knows it's being watched.

AST transformation can't do this as cleanly. And the reason comes down to something fundamental: Java source semantics are huge, but bytecode semantics are tiny.

Think about all the ways you can write an array store in Java source:

arr[i] = 5;
arr[i + 1] = arr[j - 1] + arr[k] * 2;
arr[getIndex()] = computeValue();
arr[map.get(key)] = list.get(i) > list.get(j) ? x : y;

With AST transformation, every one of these is a different tree shape. You'd need to pattern-match all of them, handle nested expressions, ternaries, method calls as indices, method calls as values — the combinatorial space is massive. Miss a pattern and it silently doesn't visualize. Add a new Java language feature and you have new patterns to handle.

But at the bytecode level? They're all the same thing. No matter how complex the source expression is, the compiler reduces it to: push array ref, push index, push value, IASTORE. One opcode. One interception point. Done.

This is true across the board. Field access in source can be this.left, node.left, getNode().left, nodes[i].left — in bytecode it's always GETFIELD. Collection operations go through INVOKEVIRTUAL regardless of how the receiver or arguments were computed.

The compiler already did the hard work of flattening Java's rich syntax into a small, fixed set of operations. Bytecode manipulation lets me piggyback on that work instead of reimplementing it.

I'm intercepting a handful of opcodes to cover a practically infinite surface of user code. That's the leverage bytecode gives you.

So how does this work in practice?

Under the hood

The engine is a Java agent — it hooks into the JVM before the user's class loads and rewrites bytecode using ByteBuddy and raw ASM visitors.

Here's the pipeline:

User writes code
       ↓
JVM loads the class
       ↓
Agent intercepts class loading (premain)
       ↓
ByteBuddy + ASM rewrite bytecode
       ↓
Transformed class runs normally
       ↓
Intercepted operations emit visualization commands
       ↓
Frontend renders step-by-step animation

Intercepting array access

This was the first thing I built. The tricky part isn't arrays specifically — it's the raw ASM stack manipulation underneath. When the JVM hits an IASTORE (integer array store), the stack looks like this:

Stack: [array_ref, index, value]

I need to capture all three, call my visualization callback, then let the original store happen. The problem is you can't just "peek" at the JVM stack — you have to pop values off, save them to local variable slots, do your work, then push them back.

// Simplified version of what ArrayAccessWrapper does:
// 1. Pop value and index into temp slots
super.visitVarInsn(Opcodes.ISTORE, valueSlot);
super.visitVarInsn(Opcodes.ISTORE, indexSlot);
super.visitVarInsn(Opcodes.ASTORE, arraySlot);

// 2. Call VisualizerRegistry.onArraySet(array, [index, value, lineNumber])
// ... pack args into Object[], invoke static method ...

// 3. Restore stack and execute original IASTORE
super.visitVarInsn(Opcodes.ALOAD, arraySlot);
super.visitVarInsn(Opcodes.ILOAD, indexSlot);
super.visitVarInsn(Opcodes.ILOAD, valueSlot);
super.visitInsn(Opcodes.IASTORE);  // original operation

This runs for every single array access in the user's code. int[], boolean[], double[] — all of them. The user writes arr[i] = arr[j] and both the read (IALOAD) and the write (IASTORE) fire visualization events.

Intercepting field mutations (trees and linked lists)

When someone writes node.left = new TreeNode(5), that compiles to a PUTFIELD instruction. I intercept it the same way — capture the owner object, the field name, and the line number, then call VisualizerRegistry.onFieldSet().

The registry figures out that node belongs to a tree visualizer, and the tree visualizer knows how to turn that field mutation into a "add node, add edge" command for the frontend.

The user just annotates the root with @Tree and writes normal tree code. The engine auto-detects the node class structure — finds the two self-referential fields (children) and the value field. Any class that looks like a binary tree node works.

@Tree TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);   // PUTFIELD intercepted → "add node 2, add edge 1→2"
root.right = new TreeNode(3);  // PUTFIELD intercepted → "add node 3, add edge 1→3"

Try it

AlgoFlow is live at algopad.dev. Write a sorting algorithm, build a graph, implement a tree traversal — and watch it execute step by step.

The code is open source: github.com/vish-chan/AlgoFlow

In the next post, I'll cover the gnarlier problems — how I got bytecode interception working on JDK bootstrap classes like ArrayList and HashMap, the auto-detection system that eliminates manual registration, and the war stories from debugging JVM stack manipulation. Stay tuned.