Enterprise Java Persistence beyond the JPA mindset

Enterprise Java Persistence beyond the JPA mindset

For a long time, enterprise Java persistence has been approached as a largely stable area of the stack. Entities, an ORM, and a relational database were sufficient for many systems, and JPA became a widely adopted and reliable way to manage data access. That stability, however, has also shaped how persistence is still conceptualised today, even as the systems we build, and the data they rely on, have become significantly more complex.

As Otavio Santana, an award-winning software engineer, Java Champion, and long-time contributor to the Java ecosystem, says in an interview: “JPA didn’t break. JPA is still there, it’s well designed.” The real issue is not JPA itself, but the assumption that it should remain the default answer to every persistence problem.

He expands on this point by clarifying that the shift is driven by the persistence landscape itself rather than by shortcomings in JPA: “What happens is we have more flavours, more databases, more solutions around persistence in the enterprise. JPA is just one more flavour of a much bigger persistence universe.”

Modern enterprise systems no longer operate in a single-database world. Teams routinely combine relational databases, NoSQL stores, read-heavy APIs, and specialised data services. Persistence has become polyglot by necessity, not by trend. Yet many architectures are still designed around a JPA-centric mental model that struggles to adapt once systems grow, diversify, or change direction.

 

The Evolution of Persistence in Enterprise Java

This is the gap Jakarta EE 12 explicitly addresses. In his OCX 26 session, From JDO to Jakarta Query: The Evolution of Persistence in Enterprise Java, Otavio Santana will discuss how enterprise Java persistence evolved from entity-centric models to today’s repository and query-driven approach, and why that shift matters for systems being built now.

Rather than replacing JPA, Jakarta EE 12 expands the persistence toolbox. Jakarta Data introduces a more declarative, repository-centric approach that allows developers to focus on domain logic instead of persistence mechanics. Jakarta NoSQL recognises non-relational databases as first-class citizens in enterprise Java. Jakarta Query enables queries to be expressed in a storage-agnostic way, allowing providers to translate and optimise them for the underlying data store.

The shift is subtle but important. As Otavio explains, the goal is to be “more domain-centric” without sacrificing performance. Persistence becomes an implementation detail again, not something that dictates how the domain must be modelled.

The cost of clinging to outdated persistence assumptions is real: excessive boilerplate, tightly coupled domain code, and architectural decisions that become expensive to undo. Jakarta Data and Jakarta Query are not about abstraction for abstraction’s sake; they are about reducing cognitive load and making persistence choices explicit, flexible, and easier to evolve.

If you are making persistence decisions today that need to hold up over the next few years, this session is worth attending in person. You’ll gain a clear mental model for modern enterprise Java persistence, when JPA still fits, when it doesn’t, and how Jakarta Data and Jakarta Query change the trade-offs. You’ll leave with a practical framework for designing persistence layers that are domain-centric, polyglot, and ready for change.

 

If you haven’t already, register to attend OCX 26 in Brussels. 

Image
OCX

 

Daniela Nastase


I Turned 6 Dusty Vercel Projects Into 7 Gumroad Products in One Day

The Problem

I had 6 Next.js projects deployed on Vercel. Landing pages, dashboards, portfolio templates, an OG image generator — all built while learning.

They were just… sitting there. Getting zero traffic. Costing me nothing but also earning nothing.

The Idea

What if I packaged them as products? Developers buy templates and starter kits all the time. I already had the code. I just needed to make it sellable.

What I Did

In one day, I:

  1. Audited each project — checked code quality, removed personal stuff, added docs
  2. Created a Gumroad store (free, $0 to start)
  3. Wrote product descriptions with feature lists, tech stacks, and live demos
  4. Priced them between $9 and $49
  5. Published 7 products

The Products

Here’s what I listed:

Product Price Stack
LaunchFast SaaS Kit $49 Next.js 16, Stripe, Auth
Admin Dashboard $49 Next.js, Clerk, Prisma
Landing Templates (5-pack) $25 Next.js, Tailwind CSS v4
HeatQuote Starter $20+ Next.js, dynamic pricing
Dev Portfolio $19 Next.js, Framer Motion
Anti-AI UI Components $15 React, Tailwind CSS v4
OGSnap $9 Next.js, OG images

All built with Next.js + Tailwind CSS v4. All deployed and demo-able on Vercel.

Key Decisions

Why Gumroad? Zero upfront cost. No monthly fee. They take a cut only when you sell.

Why these prices? I looked at similar products. SaaS kits go for $49-199. Templates for $15-39. I priced on the low end since I’m unknown.

Why “Zero AI smell”? Every template today looks the same — rounded corners, indigo accents, Inter font. I deliberately designed mine to look different.

Results So Far

Honestly? $0 in sales. It’s been a few hours.

But I now have:

  • A live store: binbreeze3.gumroad.com
  • 7 products with live demos
  • A foundation to market from

What’s Next

  • More marketing (X, dev communities)
  • Building a new SaaS template with a distinctive design
  • Writing more about the journey

Takeaway

If you have side projects collecting dust, consider packaging them as products. The code is already written. The hard part is the marketing.

What’s your experience selling dev tools? I’d love to hear from people who’ve done this before.

Savior: Low-Level Design

Grinding Go: Low-Level Design

I went back to the drawing board for interview preparation and to sharpen my problem-solving skills. Software development is in a weird stage. 2 weeks ago, when I saw my friend doing low-level-design, I thought that in current stage it is meaningless. How wrong I was. My friend started asking me about problem solving and critical thinking. I realized that I am killing my skills by only focusing on abstraction nowadays and learning new tools that maybe in a new job I will never use. For that reason, I did research and saw that a pretty good amount of devs are complaining about how inline suggestions on IDE and AI tools kill their problem-solving skills. I watched this video which is related to current interview style by NeetCode. I realized that nothing has changed pretty much and problem-solving skills stay as the strongest skills. That’s why, I create a new repository: grinding-go.

Self Reflection

My friend and I have been starting to interview each other for low-level-design and system design. I noticed that my critical thinking has got softer. While making our first interviews, I was mumbling. I wasn’t giving details because of my half-formed ideas. So, I planned a strategy for this current stage of my coding skills: Do low-level design barehand and ask LLM to find articles about the design and make it ask socratic follow-up questions for my design in order to sharpen my problem solving skills. Therefore, I went back to the drawing board to do low-level design for problems that are abstracted away and are just big word alerts. I also realized again, algorithms and computational thinking don’t go anywhere, they just stay under abstractions in large codebases which if I don’t keep my skills up to date with them, I’ll struggle a lot.

The Main Resource

I created grinding-go project in Go because I love this language for its simplicity and speed. I started reading the summary of 100 Go Mistakes and coding snippets which are in this repo via this website. I realized as a developer, I need to come back to my basics and make them as strong as possible by doing also boring stuff. After that book, I would definitely say that I stopped treating Go like other languages because it has its own style especially when it comes to error handling and concurrency.

First Technical Challenge: Implement LRU Cache

When it comes to my core structure, I used LRUCache struct which contains LinkedList and mutex for thread-safety and concurrency:

type LRUCache struct {
    mu       sync.RWMutex
    capacity int
    cache    map[int]*Node
    list     *LinkedList
}

Here, the main purpose is to make a quick look-up with map and when we change the position of the least recently used item we use Linkedlist which doesn’t take O(n). Also we add a new item into the lru cache if it passes the capacity we need to remove the tail of the linked list which takes O(1) operation.

if len(lru.cache) > lru.capacity {
        tailKey, err := lru.list.removeTail()
                // ...
}

While solving this problem, I forgot to nil out the removed element’s pointers from the linkedList. Now in Go this is not a “dangling pointer” like in C/C++ since Go has a garbage collector. But it still matters because the removed node can hold references to other nodes in the list which prevents the GC from freeing memory that should be freed. It can also cause logical bugs if you accidentally traverse through stale references:

// clear dangling pointers
    n.prev = nil
    n.next = nil

Meanwhile handling the neighbours of linked list elements, I noticed that I am making mistakes and losing the references to elements. I wrote everything in one file which reminded me of solving problems in one file rather than abstracting away everything. I first heard this approach from ThePrimeAgen: solve it in one file first, then split later. LRU Cache is everywhere in production anyway. What I don’t get is when people tell that solving these problems is dead. It isn’t dead. In production and large codebases the same patterns and data structures are being used with over abstraction and if you don’t know at low level, it will be more difficult.

Rate Limiter

When I started this problem, I directly thought about Sliding window technique but I already used this technique in different projects and I wanted to do it with Token Bucketing and for that reason I chose it. At the end of the day, we as developers have to challenge ourselves and see how it goes. I already got the basic idea of it and meanwhile solving it I made searches about the main difference between Token Bucketing and Sliding window for checking out the trade-offs again. I watched this video and started implementing it. I implemented a RateLimiter struct which has a composition relationship with TokenBucket:

type RateLimiter struct {
    buckets   map[string]*TokenBucket // maps a user/key to their bucket
    mu        sync.Mutex              // Protects concurrent map access
    rate      float64                 // default rate for new buckets
    maxTokens float64                 //default burst size for new buckets
}

I also wrote in TokenBucket struct a mutex field because it has to handle its own operation concurrently and I implemented Allow method which controls how many requests a client can make during a specific time frame:

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()

    now := time.Now()
    tb.lastSeen = now
    newTokens := now.Sub(tb.lastRefill).Seconds() * tb.refillRate
    tb.tokens += newTokens
    if tb.tokens > tb.maxTokens {
        tb.tokens = tb.maxTokens
    }
    tb.lastRefill = now
    if tb.tokens >= 1 {
        tb.tokens--
        return true
    }
    return false
}

After doing that I was happy and thought that I’m done but when it comes to socratic question I saw that I’m not cleaning up idle buckets in RateLimiter. Without cleanup, if you have millions of unique keys hitting your limiter, those buckets just sit in memory forever. So I added a cleanUpLoop that periodically evicts stale entries:

func (rl *RateLimiter) cleanUpLoop(interval, maxIdle time.Duration) {
    ticker := time.NewTicker(interval)

    for range ticker.C {
        rl.mu.Lock()
        for key, tb := range rl.buckets {
            if time.Since(tb.lastSeen) > maxIdle {
                delete(rl.buckets, key)
            }
        }
        rl.mu.Unlock()
    }
}

Since I mentioned the trade-offs between token bucket and sliding window, here is a short comparison:

Token Bucket:

  • Tokens refill at a fixed rate, each request costs one token
  • Allows bursts up to bucket size which is good for APIs like payment or webhook endpoints
  • Low memory, just a counter and a timestamp

Sliding Window:

  • Tracks requests in a time window and counts them
  • Smoother and stricter rate control, no big bursts
  • Higher memory since you need to store timestamps or counters per sub-window

Pub/Sub

As you already know in system design interviews especially when it comes to microservices architecture Pub/Sub is mentioned a lot. Because of its asynchronous nature and substantial workload with rest APIs pub/sub makes sense to use to reduce tight coupling and solve the problem by also keeping the performance good. But, I never thought about its implementation details and when it comes to learning anything, my strategy is to write the code with my naive solution and research about it during building my solution. I watched this video by Hussein Nasser which really helped me a lot. Especially those who want to prioritize concepts over tools, I highly recommend this person’s videos.

Types

there are 4 core structs which are:

  • Topic: this groups related messages
  • Subscriber: this is the one receives messages.
  • Broker: knows which subscribers care about which topics and route messages to them
  • Message: is just the data

Broker plays a middle man role and its methods delegate to Topic methods and return their errors directly. When I first wrote Unsubscribe method, I used Broker’s mutex via defer which was a poor decision because the lock was only needed for the topic lookup not for topic-level operations. Holding the broker lock while doing topic operations would block all other broker methods unnecessarily. Then I changed it properly:

func (b *Broker) Unsubscribe(topicName string, sub *Subscriber) error {
    b.mu.Lock()
    t, ok := b.topics[topicName]
    b.mu.Unlock() // release broker lock before touching topic
    if !ok {
        return ErrTopicNotFound
    }
    return t.RemoveSubscriber(sub.id)
}

Also, in the Broadcast method I made sure to pass the subscriber as a parameter to the goroutine. Without this, it becomes a closure bug where all goroutines can end up referencing the last value of the loop variable. Since Go 1.22, the loop variable scoping changed and each iteration gets its own copy. So this specific closure bug is fixed in Go 1.22+. But passing the variable as a parameter is still a good habit, it makes intent clear and keeps your code compatible with older versions. You can read about it in the official Go blog post.

    for _, sub := range t.subscribers {
        // yo I need to  use subscriber as a parameter because in another case it is closure bug
        // Because  by the time goroutine runs, the for loop may have moved and sub points to the last sub
        go func(s *Subscriber) {
            select {
            case s.ch <- msg:
            default:
            }
        }(sub)

In this problem, I made a PR to my own project and made my friend to review it in order to get a different perspective and he gave me feedback. At the end of the day, these are just tools and I should focus more on architecture rather than some fancy words about tools.

Lastly

I will keep this as a series and after each 3 low-level-design, I will come with my naive solutions to these big word alerts. I reminded myself of my engineering skills by coming back to the drawing board, thinking about the problems in low level and understanding trade-offs while implementing it. Nowadays my friend and I are so busy with job-hunting and we are planning to write about our project verdis. In my previous blog, I mentioned that we had our first podcast which looked like we filmed it with a fridge camera but we already did our second podcast and this time is way better :). I added a new technique into my formula besides building in open and contributing also solving the problems with low level design. If you are curious about it and if you see anything in my solutions feel free to give any suggestions and if you know any technical challenges to recommend, feel free to create issues in grinding-go.

From Data to Decisions: How Augmented Analytics is Transforming Business

Data science is rapidly evolving, moving beyond traditional analytics to a more collaborative approach known as augmented data science. This method integrates human expertise with AI and machine learning models to generate insights that are not only accurate but contextually meaningful and ethically sound. Unlike fully automated systems, augmented data science emphasizes the partnership between humans and intelligent algorithms, ensuring better decision-making in complex environments.
Having worked on predictive and prescriptive analytics across finance, healthcare, and marketing domains, I’ve observed that human judgment combined with AI capabilities produces more reliable and actionable insights. Organizations today are no longer satisfied with raw outputs—they want informed, interpretable, and strategic recommendations that can guide high-stakes decisions.
What Is Augmented Data Science?
Augmented data science refers to systems where AI models assist humans in analyzing data, uncovering patterns, and making decisions. It leverages the strengths of both:
• AI Models: Handle vast datasets, uncover hidden patterns, and generate predictive insights.
• Human Expertise: Provides domain knowledge, ethical judgment, and contextual understanding that algorithms alone cannot offer.
For instance, in healthcare, AI may flag potential diagnoses from imaging data, but medical professionals interpret these results within the broader clinical picture. In finance, models may predict risk exposure, while analysts incorporate regulatory knowledge and economic context. This collaboration ensures decisions are both data-driven and practically grounded.
The Importance of Human-AI Collaboration
Traditional data science relied heavily on human analysts for every step, from data cleaning to modeling and interpretation. Fully automated AI models, while efficient, often lack transparency and contextual awareness. Augmented data science addresses these gaps by combining AI speed and precision with human judgment.
Key benefits include:
• Enhanced Accuracy: Human review minimizes errors and validates model assumptions.
• Faster Insights: AI handles repetitive tasks, allowing humans to focus on strategic interpretation.
• Ethical Oversight: Humans ensure fairness, transparency, and responsible AI deployment.
• Contextual Relevance: Experts interpret model outputs with domain knowledge for actionable outcomes.
Industry reports suggest that organizations adopting augmented approaches outperform peers in decision speed, accuracy, and stakeholder trust.
Tools and Techniques Driving Augmented Data Science
Several technologies empower augmented workflows:
• Automated Machine Learning (AutoML): Tools like H2O.ai and Google Cloud AutoML accelerate model creation while keeping humans in control.
• Natural Language Processing (NLP): Converts unstructured text into actionable insights.
• Visual Analytics Platforms: Tools such as Tableau and Power BI help interpret AI-generated insights interactively.
• Explainable AI (XAI): Methods like SHAP and LIME allow humans to understand and trust complex models.
Organizations seeking to implement these systems often encourage employees to enhance their skills through the best data science course, which provides hands-on training in machine learning, model evaluation, and human-in-the-loop analytics.
Human-AI Workflow in Practice
A typical augmented data science workflow includes:

  1. Data Preparation: AI assists with cleaning, feature selection, and normalization.
  2. Model Development: Humans define objectives, select algorithms, and validate assumptions.
  3. Interpretation: Experts analyze AI outputs to identify anomalies and contextualize insights.
  4. Decision-Making: Humans make final decisions, integrating ethical, operational, and strategic considerations.
  5. Feedback Loop: Human evaluation informs iterative model improvements.
    This collaborative cycle ensures continuous improvement, where AI and human expertise complement each other.
    Industry Applications
    Augmented data science is transforming multiple sectors:
    • Healthcare: AI predicts patient risk, while clinicians contextualize diagnoses and treatment plans.
    • Finance: Models flag credit risks or market opportunities, supplemented by analysts’ domain knowledge.
    • Retail and Marketing: AI identifies consumer trends, and human teams adjust campaigns based on brand strategy.
    • Manufacturing: Predictive maintenance models highlight risks, with engineers providing operational insights.
    These applications demonstrate that combining human intuition with AI capabilities improves both the reliability and applicability of insights.
    Ethical Considerations
    While augmented data science enhances efficiency, it also raises ethical responsibilities:
    • Bias Mitigation: Human oversight ensures AI predictions do not perpetuate discrimination.
    • Privacy Compliance: Experts ensure responsible data collection and storage.
    • Accountability: Decisions influenced by AI are traceable and auditable.
    Ethics are particularly crucial in domains like healthcare and finance, where incorrect predictions can have significant consequences. Human intervention ensures these systems remain responsible and socially acceptable.
    Emerging Trends
    Current trends in augmented data science include:
    • Explainable AI (XAI): Increasing demand for transparency and interpretability.
    • Cloud-Based AI Collaboration: Real-time integration of human and AI workflows.
    • Data Literacy Programs: Organizations are training employees to understand AI outputs effectively.
    • Hybrid Intelligence Models: Combining multiple AI models with human oversight for complex problem-solving.
    These developments indicate a future where AI augments human intelligence rather than replaces it.
    Growth and Learning Opportunities
    The demand for skilled professionals in augmented data science is growing rapidly. Organizations in major technology hubs are seeking employees who can combine technical expertise with domain knowledge and ethical judgment. Many learners now explore structured programs such as a Data science course in Chennai, which equips them with skills in machine learning, data visualization, and human-AI collaboration.
    Such programs emphasize the balance between computational efficiency and human interpretive skills, preparing students to operate effectively in environments where AI and human expertise work hand-in-hand.
    Conclusion
    Augmented data science is redefining the analytics landscape by integrating human judgment with AI models to produce insights that are accurate, actionable, and ethically responsible. This collaborative approach improves decision quality, ensures fairness, and strengthens stakeholder trust. As organizations increasingly adopt AI-driven workflows, professionals with expertise in both technical and human-centered analytics are in high demand. Many aspiring data scientists are now enrolling in the Artificial Intelligence Course in Chennai, which focuses on hands-on AI modeling, interpretability, and ethical decision-making. By combining human expertise with advanced models, augmented data science is poised to transform how businesses derive insights and make informed decisions.

Your First Azure Function: HTTP Triggers Step-by-Step

Theory doesn’t ship features. You learned the why in Part 1—when serverless makes sense, the economics, how it compares to App Service and Container Apps. Now let’s build something.

In this second part of the Azure Functions for .NET Developers series, we’ll create a project from scratch, understand every line of generated code, and build three HTTP trigger patterns that cover most real-world API scenarios. By the end, you’ll have a working HTTP API running locally on your machine. No Azure subscription required.

All code from this article is available in the azure-functions-samples repository.

Prerequisites

You need four tools installed before we start. Here’s what to check and where to get them:

Tool Verify Expected
.NET 10 SDK dotnet --version 10.x
Azure Functions Core Tools func --version 4.x
VS Code + Azure Functions extension Latest
Azurite npx azurite --version Any

.NET 10 SDK is required to build and run our function app. It’s an LTS release, supported through November 2028. Grab it from dot.net/download if you don’t have it.

Azure Functions Core Tools v4 lets you create, run, and debug functions locally. Install via npm, Homebrew, or Chocolatey:

# npm (any OS)
npm install -g azure-functions-core-tools@4

# Homebrew (macOS)
brew tap azure/functions
brew install azure-functions-core-tools@4

# Chocolatey (Windows)
choco install azure-functions-core-tools-4

VS Code with the Azure Functions extension gives you project templates, debugging, and deployment in one place. Visual Studio and Rider work too—use whatever you’re comfortable with. The CLI commands in this article work regardless of editor.

Azurite emulates Azure Storage locally. The Functions runtime needs a storage connection even for HTTP triggers (it uses storage internally for features like function key management and, in production, for coordinating across instances). Install it globally or run it on demand:

# Install globally
npm install -g azurite

# Or run on demand without installing
npx azurite

If the verification commands all return version numbers and you have VS Code with the Azure Functions extension installed, you’re ready.

Creating Your First Project

With your tools in place, open a terminal and scaffold a new function app:

func init HttpTriggerDemo --worker-runtime dotnet-isolated --target-framework net10.0
cd HttpTriggerDemo
func new --template "HTTP trigger" --name Hello

The first command creates the project. --worker-runtime dotnet-isolated selects the isolated worker model (the modern default—we’ll explain what “isolated” means in Part 5). --target-framework net10.0 targets .NET 10.

Once inside the project directory, func new adds an HTTP-triggered function called Hello from a built-in template.

Your project now looks like this:

HttpTriggerDemo/
├── Hello.cs              # Your function code
├── Program.cs            # Application entry point
├── HttpTriggerDemo.csproj # Project file
├── host.json             # Runtime configuration
└── local.settings.json   # Local environment variables

The Project File

Open HttpTriggerDemo.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.51.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
  </ItemGroup>
</Project>

A few things to note:

  • OutputType: Exe — the isolated worker model runs as a standalone executable, not a class library loaded into the Functions host. This is what gives you full control over the process.
  • FrameworkReference: Microsoft.AspNetCore.App — brings in ASP.NET Core types like HttpRequest and IActionResult. Without this, you’d use the lower-level HttpRequestData API.
  • Extensions.Http.AspNetCore — the bridge that maps ASP.NET Core’s HTTP abstractions to the Functions runtime. This is what makes the developer experience feel like writing a regular web API.
  • ImplicitUsings: enable — the compiler automatically includes common using statements (System, System.Collections.Generic, System.Linq, System.Threading.Tasks, and others). That’s why our code files don’t start with a block of using directives.
  • Nullable: enable — turns on nullable reference types. The compiler warns you when code might dereference null without checking. You’ll see this in action when we read query string values—req.Query["name"] returns string?, not string.

The Entry Point

Program.cs is minimal:

using Microsoft.Azure.Functions.Worker.Builder;

var builder = FunctionsApplication.CreateBuilder(args);

builder.ConfigureFunctionsWebApplication();

builder.Build().Run();

ConfigureFunctionsWebApplication() is the key line. It enables ASP.NET Core integration—without it, you’d need to use HttpRequestData and HttpResponseData instead of the familiar HttpRequest and IActionResult. For HTTP-triggered functions, this is almost always what you want.

FunctionsApplication.CreateBuilder(args) is a convenience factory introduced in Worker SDK 2.0. It returns a pre-configured IHostApplicationBuilder with Functions defaults already wired up—logging, JSON serializer options, and middleware. The key using to remember is Microsoft.Azure.Functions.Worker.Builder, which brings the factory method into scope. You may also see an older pattern that starts with new HostBuilder() and calls .ConfigureFunctionsWorkerDefaults()—it still works, but requires more manual setup (for example, explicit ConfigureAppConfiguration() calls to load appsettings.json). func init scaffolds FunctionsApplication.CreateBuilder by default, so stick with it for new projects.

This is also where you’d register services for dependency injection, add middleware, or configure logging. We’ll keep it simple for now.

Understanding the Generated Code

With the project structure in place, let’s look at the function itself. The func new command created a file called Hello.cs with a complete working function. Before we run it, let’s understand what each part does. Open Hello.cs:

public class Hello(ILogger<Hello> logger)
{
    [Function("Hello")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
    {
        logger.LogInformation("C# HTTP trigger function processed a request.");
        return new OkObjectResult("Welcome to Azure Functions!");
    }
}

Let’s break this apart.

public class Hello(ILogger<Hello> logger) uses a C# 12 primary constructor. The ILogger<Hello> parameter is injected by the dependency injection container automatically—no need to write a separate constructor or field assignment. The logger is available throughout the class.

[Function("Hello")] registers this method with the Functions runtime. The string "Hello" becomes the function’s name and appears in the URL path: http://localhost:7071/api/Hello. If you rename the class but keep the attribute string the same, the URL stays the same.

[HttpTrigger(AuthorizationLevel.Function, "get", "post")] is where the behavior is defined:

  • AuthorizationLevel.Function means callers need a function-specific API key to invoke this endpoint. Other options are Anonymous (no key required) and Admin (requires the master host key). When running locally, authorization is bypassed regardless of the level you set.
  • "get", "post" lists the allowed HTTP methods. Requests using other methods receive a 405 Method Not Allowed.

HttpRequest req is the full ASP.NET Core request object. Query strings, headers, body, route values—it’s all there. If you’ve written ASP.NET Core controllers or minimal APIs, this is the same type.

IActionResult is the return type. OkObjectResult maps to a 200 response. The runtime serializes the object to JSON (or returns it as plain text for strings) and sends it back to the caller. All the familiar result types work: BadRequestResult, NotFoundResult, CreatedResult, and so on.

Run It

Before we modify anything, let’s see this function in action. Start Azurite in a separate terminal (or in the background):

npx azurite --silent --location /tmp/azurite &

Then start the Functions runtime from your project directory:

func start

You should see output listing your function’s URL:

Functions:

        Hello: [GET,POST] http://localhost:7071/api/Hello

Test it in another terminal:

curl http://localhost:7071/api/Hello
# → Welcome to Azure Functions!

That’s a working HTTP endpoint, running locally, no Azure subscription needed. Keep func start running—we’ll build on this function next.

HTTP Trigger Deep Dive

With a running function under your belt, let’s build three patterns that cover the majority of real-world HTTP function scenarios. After each change, stop func start with Ctrl+C and restart it to pick up the new code.

Pattern 1: GET with Query String

The simplest pattern—read a value from the URL and return a response:

public class HelloFunction(ILogger<HelloFunction> logger)
{
    [Function("Hello")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req)
    {
        logger.LogInformation("Hello function triggered");

        string? name = req.Query["name"];
        return new OkObjectResult($"Hello, {name ?? "world"}!");
    }
}

req.Query["name"] reads the name parameter from the query string. If it’s missing, the value is null, so we fall back to "world" with the null-coalescing operator. Note the string? — the query collection returns a nullable type, and with <Nullable>enable</Nullable> in the project file, the compiler enforces this.

Test it:

curl "http://localhost:7071/api/Hello?name=Azure"
# → Hello, Azure!

curl "http://localhost:7071/api/Hello"
# → Hello, world!

This pattern works well for simple lookups—search queries, filtering lists, or any GET request where the parameters are short and cacheable.

Pattern 2: POST with JSON Body

For creating or updating resources, you’ll typically accept a JSON request body. C# records and ASP.NET Core model binding make this clean:

using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute;

public record CreateOrderRequest(string ProductId, int Quantity);

public class OrderFunction(ILogger<OrderFunction> logger)
{
    [Function("CreateOrder")]
    public IActionResult CreateOrder(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders")] HttpRequest req,
        [FromBody] CreateOrderRequest order)
    {
        logger.LogInformation("Order for {ProductId} x{Quantity}", order.ProductId, order.Quantity);
        return new CreatedResult($"/orders/{Guid.NewGuid()}", order);
    }
}

Several things happening here:

record CreateOrderRequest defines an immutable data type. Records are ideal for request and response DTOs—they give you value equality, a clean ToString(), and immutability out of the box. No need to write a class with properties and a constructor.

[FromBody] CreateOrderRequest order tells the ASP.NET Core model binder to deserialize the JSON request body into a CreateOrderRequest instance. If the JSON doesn’t match (missing required fields, wrong types), the runtime returns a 400 Bad Request automatically. One gotcha: with ASP.NET Core integration enabled, FromBody exists in two namespaces—Microsoft.Azure.Functions.Worker.Http and Microsoft.AspNetCore.Mvc. You need the Functions Worker version. The using alias at the top of the file resolves the ambiguity so you can use [FromBody] without a fully qualified name.

Route = "orders" overrides the default route. Without it, the URL would be /api/CreateOrder (derived from the function name). With it, the URL becomes /api/orders—cleaner and more RESTful.

CreatedResult returns HTTP 201 with a Location header pointing to the new resource. Standard REST semantics.

Test it:

curl -X POST http://localhost:7071/api/orders 
  -H "Content-Type: application/json" 
  -d '{"productId":"SKU-001","quantity":3}'
# → {"productId":"SKU-001","quantity":3}
# (with 201 status and Location header)

The Content-Type: application/json header is required. Without it, model binding won’t kick in and you’ll get a deserialization error.

Pattern 3: Route Parameters

For resource-oriented APIs, you’ll want parameters embedded in the URL path rather than the query string:

public class ProductFunction(ILogger<ProductFunction> logger)
{
    [Function("GetProduct")]
    public IActionResult GetProduct(
        [HttpTrigger(AuthorizationLevel.Function, "get",
            Route = "products/{category:alpha}/{id:int?}")] HttpRequest req,
        string category, int? id)
    {
        logger.LogInformation("Looking up {Category}, id={Id}", category, id);
        return new OkObjectResult(new { category, id });
    }
}

Route parameters are defined in curly braces inside the Route string and captured as method parameters matched by name. The runtime extracts category and id from the URL and passes them directly to your method.

Route constraints validate parameters before your code runs:

Constraint Meaning Example
:alpha Letters only electronics matches, 123 doesn’t
:int Integer 42 matches, abc doesn’t
:int? Optional integer Parameter can be omitted
:guid GUID format {id:guid}
:length(5) Exact string length {code:length(5)}
:min(1) Minimum integer value {page:min(1)}

If a constraint fails, the runtime returns a 404—your function never executes.

Test it:

curl http://localhost:7071/api/products/electronics/42
# → {"category":"electronics","id":42}

curl http://localhost:7071/api/products/electronics
# → {"category":"electronics","id":null}

curl http://localhost:7071/api/products/123/42
# → 404 (constraint :alpha rejects "123")

You now have three working patterns that cover most real-world HTTP function scenarios.

Running and Testing Locally

Restart func start now that all three functions are in place. You should see all of them listed:

Azure Functions Core Tools
Core Tools Version: 4.6.0

Functions:

        CreateOrder: [POST] http://localhost:7071/api/orders

        GetProduct: [GET] http://localhost:7071/api/products/{category:alpha}/{id:int?}

        Hello: [GET] http://localhost:7071/api/Hello

For quick reference, here are all three test commands together:

# Pattern 1: GET with query string
curl "http://localhost:7071/api/Hello?name=Azure"

# Pattern 2: POST with JSON body
curl -X POST http://localhost:7071/api/orders 
  -H "Content-Type: application/json" 
  -d '{"productId":"SKU-001","quantity":3}'

# Pattern 3: Route parameters
curl http://localhost:7071/api/products/electronics/42

You can also use Postman, the REST Client extension for VS Code, or HTTPie—whatever fits your workflow.

A note on authorization: when running locally, AuthorizationLevel.Function is bypassed. You don’t need to pass a function key. This makes local development frictionless. Once deployed, clients will need to include the key as a query parameter (?code=<key>) or in the x-functions-key header.

Troubleshooting

If things don’t work on the first try, check these common issues:

“Can’t determine project language” or “No job functions found”

The .csproj file is missing required settings. Verify it targets net10.0, has <AzureFunctionsVersion>v4</AzureFunctionsVersion>, and includes the Worker SDK packages. Run dotnet build separately to check for compilation errors.

Port 7071 already in use

Another Functions host (or another process) is using the default port. Either kill it or start on a different port:

func start --port 7072

“Value cannot be null: provider” or storage connection errors

Azurite isn’t running. The Functions runtime needs a storage emulator even for HTTP triggers. Start Azurite, or verify that local.settings.json contains:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}

UseDevelopmentStorage=true tells the SDK to connect to Azurite on its default ports.

JSON deserialization errors on POST requests

Two common causes: missing the Content-Type: application/json header, or property name mismatches between your JSON and your C# record. By default, the serializer is case-insensitive, so productId and ProductId both work. But the property names must exist on the target type.

“The listener for function X was unable to start”

Usually a runtime version mismatch. Confirm func --version returns 4.x and your .csproj has <AzureFunctionsVersion>v4</AzureFunctionsVersion>. If you recently upgraded the Core Tools, also run dotnet clean and rebuild.

Conclusion

We went from an empty directory to a running HTTP API with three production patterns:

  1. Query strings for simple reads (GET /api/Hello?name=Azure)
  2. JSON bodies for mutations (POST /api/orders)
  3. Route parameters for resource-oriented endpoints (GET /api/products/electronics/42)

All running locally, no Azure subscription needed.

The isolated worker model with ASP.NET Core integration gives you familiar types—HttpRequest, IActionResult, [FromBody]—so writing Azure Functions feels like writing any other .NET web API. The main difference is that Azure handles the hosting, scaling, and infrastructure.

The code samples in this article are deliberately simple. In a real project, you’d add input validation, error handling, and probably connect to a database or external service. But the patterns are the same—the [HttpTrigger] attribute, the route configuration, the model binding. Once you understand these building blocks, everything else is regular C#.

All code from this article is available in the azure-functions-samples repository—clone it, run func start, and experiment.

What’s Next in This Series

Part 3: Beyond HTTP Triggers (coming soon) covers timer triggers for scheduled jobs, queue triggers for message processing, and blob triggers for reacting to file uploads. Same project structure, new event sources.

Part 4: Local Development Setup (coming soon) digs into the day-to-day workflow—debugging in VS Code, hot reload, and productivity tips.

Part 5: Understanding the Isolated Worker Model (coming soon) explains what “isolated” means, why Microsoft created this model, how it differs from the legacy in-process model, and what it means for your code.

And yes—we’ll cover deploying to Azure with CI/CD via GitHub Actions later in the series, so everything you’re building locally will make it to the cloud.

Azure Functions for .NET Developers Series

  • Part 1: Why Azure Functions? Serverless for .NET Developers
  • You are here: Part 2: Your First Azure Function: HTTP Triggers Step-by-Step
  • Part 3: Beyond HTTP Triggers (coming soon)

JetBrains Academy – February Digest

Hi there 👋

February is in full swing, and we’ve packed this digest with things you don’t want to miss.
Inside: upcoming events, hands-on learning, competitions, scholarships, and fresh updates to keep you moving forward in 2026 🚀

How octorus Renders 300K Lines of Diff at High Speed

In a previous post, I introduced my TUI tool. This time, I’d like to talk about the performance optimizations behind octorus.

What Do We Mean by “Fast”?

“Fast” can mean many things. Even just for rendering, there’s initial display speed, syntax highlighting speed, scroll smoothness (fps), and more.

Perceived speed and internal speed aren’t always the same. No matter how much you optimize with zero-copy or caching, if the PR is massive, the API call becomes the bottleneck. And without rendering-level optimizations, the UI can freeze entirely.

In octorus, I push internal optimizations as far as possible while also applying web-app-style thinking (FCP / LCP / INP) to the TUI.

Core Concept

The fundamental approach is to asynchronously build and consume caches based on the current display state. By maintaining 5 layers of caching, the perceived initial display time approaches 0ms, while also improving fps and minimizing allocations.

Session Cache for PR Data

PR data is fetched via gh api when a PR is opened. The fetched diff and comment data are cached in memory. This cache remains valid for the entire octorus session — even when switching between PRs. Each PR is fetched only once.

The cache isn’t unlimited; when the maximum entry count is reached, the oldest entries are evicted.

src/cache.rs#L98-L104

Background Processing for Diff Cache Construction

When a PR is opened and data arrives, diff parsing and syntax highlighting begin asynchronously in the background. By the time the user actually opens a diff, most of the work is already done — making the perceived latency effectively 0ms. This is the single biggest win for user experience.

src/app.rs#L792-L831

DiffCache Construction

The background processing aims to finish before the user opens a diff, but what happens when the diff is enormous — hundreds of thousands of lines — or when the language has complex syntax (like Haskell), making highlighting significantly heavier? Blocking the user until processing completes would be a terrible experience.

To solve this, octorus can display diffs in a plain (unhighlighted) state while highlighting is still in progress. Once highlighting completes, the view seamlessly transitions to the fully highlighted version. Here’s an example with a 300K-line diff:

The cache exists in two locations: the active display cache (the file currently being viewed) and the prefetched standby store (pre-built in the background).

When a file is selected, the system first checks if the active cache can be reused (e.g., the user is just scrolling within the same file). If not, it pulls from the standby store (if prefetching finished in time). If neither is available, it builds the cache on the spot.

When switching from File A to File B, the diff_cache is replaced with File B’s cache. File A’s cache remains in the standby store, so switching back to File A hits Stage 2 and restores instantly.

This cache is scoped per PR. Unlike the session-level API cache, it’s discarded when switching PRs. Since octorus also supports opening a PR directly by number, this design keeps the overall behavior consistent — diff_cache is bound to a single PR’s lifetime.

Efficient Highlighting via CST + Semantic Boundary-Level Interning

So far I’ve covered caching of diff data itself. Now let’s talk about optimizing the highlighting process.

Each line in DiffCache is stored as a sequence of styled Spans. If each Span naively held a String, every occurrence of the same token would trigger a separate allocation. To avoid this, I adopted lasso::Rodeo, a string interner.

An interner returns the same reference for identical strings. So even if let appears hundreds of times, only one copy exists in memory.

A typical String takes 24 bytes (pointer + length + capacity) plus ~8 bytes for the highlight style — about 32 bytes total. A lasso::Rodeo reference (Spur) is just 4 bytes.

This reduces not only per-Span size but also eliminates duplication. For a 1,000-line diff where let appears 200 times:

String Rodeo
Reference × 200 24 B × 200 = 4,800 B 4 B × 200 = 800 B
String body 3 B × 200 = 600 B 3 B × 1 = 3 B
Total 5,400 B 803 B

However, the effectiveness of interning depends heavily on granularity. Interning entire lines yields near-zero deduplication; interning individual characters makes the management overhead dominate.

The key insight is to reuse tree-sitter captures as the interning boundary. octorus parses source code extracted from diffs using tree-sitter (the same engine used in Zed, Helix, and other editors).

tree-sitter parses source code into a CST (Concrete Syntax Tree) and returns captures like @keyword, @function.call, etc. These correspond precisely to semantic units of programming languages (fn, let, {, …) — making them an ideal granularity for interning.

In other words, tree-sitter provides both the style information for highlighting and the optimal split boundaries for interning.

During initial cache construction, a Rodeo for plain diff is initialized, and tokens like + and - are interned with their fixed colors. This is what enables the “display plain first” behavior mentioned earlier. Meanwhile, a highlighted Rodeo is built in the background.

src/ui/diff_view.rs#L32-L91

Since Rodeo internally uses an arena allocator, there’s no need for individual drops — freeing the arena frees all interned strings at once. Furthermore, the Rodeo is moved into DiffCache, so it’s bound to the DiffCache’s lifetime. When the cache is dropped, all interning data is cleanly released. The fact that tree-sitter parse/query frequency equals Rodeo cache construction frequency is another nice alignment.

src/app.rs#L88-L99

Resolving Overlapping Captures

tree-sitter captures are returned per syntax tree node, so parent and child nodes can overlap in range. For example, in #[derive(Debug, Clone)]:

  • @attribute covers the entire range [0..23)
  • @constructor individually captures Debug [9..14) and Clone [16..21)

Naively processing from the start, @attribute‘s style would advance the cursor to position 23, and the inner @constructor captures would be missed.

[0..23)  @attribute            "#[derive(Debug, Clone)]"  ← style applied to entire range
[1..2)   @punctuation.bracket  "["                         ← nested
[8..9)   @punctuation.bracket  "("                         ← nested
[9..14)  @constructor          "Debug"                     ← nested
[16..21) @constructor          "Clone"                     ← nested
[21..22) @punctuation.bracket  ")"                         ← nested
[22..23) @punctuation.bracket  "]"                         ← nested

The solution: generate independent start/end events for each capture, sort them by position, and sweep left-to-right in a single pass. Active captures are managed on a stack, so the innermost (most specific) style always takes priority.

(0,  start, @attribute)
(9,  start, @constructor) ← takes priority
(14, end,   @constructor)
             ↓ falls back to @attribute
(16, start, @constructor) ← takes priority again
(21, end,   @constructor)
(23, end,   @attribute)

The time complexity is O(m log m) where m is the number of captures — independent of line length. For minified JS with extremely long lines, this scales only with capture count, not byte length. A naive byte-map approach would require O(n) memory and traversal for line length n, so the gap widens with longer lines.

Parser and Query Caching

Some languages don’t map 1:1 from file extension to a single parser/highlight query. Vue and Svelte are prime examples — their Single File Components combine HTML, JS, and CSS in one file.

This means highlighting a single file requires initializing 3 parsers/queries. If a PR contains 50 .vue or .svelte files, that’s 150 initializations.

To solve this, once a parser/query is created, it’s stored in a ParserPool cache shared across all files. No matter how many files there are, only 3 initializations are needed. Given that some query compilations involve nearly 100KB of data, this is a non-trivial optimization.

Other Optimizations

Beyond the multi-layer cache, several smaller optimizations contribute to the overall experience.

Viewport-Restricted Rendering

octorus uses the ratatui crate for TUI rendering.

Rather than rendering all lines, only the visible range is sliced and passed to ratatui. Pre-rendering transformations (Span → Line conversion) and Rodeo string lookups are also limited to this range. Simple, but more directly impactful on perceived performance than something like ParserPool.

src/ui/diff_view.rs#L644-L655

Lazy Composition of Comment Markers

Comment data is intentionally excluded from the cache. In octorus, comments are fetched after the diff data, so they’re composed at render time via iterator composition.

src/ui/diff_view.rs#L520-L529

As a result, comment markers appear slightly after the diff viewer opens (noticeable on very large diffs).

No Moves Between Cache Construction and Rendering

The Rodeo is moved into DiffCache during cache construction, but after that, everything through rendering is purely borrowed. As mentioned earlier, since the Rodeo is owned by DiffCache, dropping the cache drops all interning data — guaranteeing no lifetime leaks across the entire pipeline. This is less of an “optimization” and more of a strength of Rust’s ownership system.

Closing Thoughts

By choosing Rust, octorus has been able to introduce optimizations incrementally. None of the techniques described here were introduced all at once — they were spread across dozens of PRs. The ability to start with a naive implementation for correctness and layer in zero-copy and multi-stage caching later is a testament to Rust’s scalability.

Beyond raw speed, octorus also features AI-Rally, a powerful AI-assisted review capability. Give it a try!

👉 github.com/ushironoko/octorus

Are We Having the Wrong AI Dreams?

(This opinion piece by JetBrains’ Team Lead in AI Development Experience reflects on key takeaways from NeurIPS 2025, a major AI research conference. It explains why these insights matter and considers related signals emerging from other recent research.)

Mass layoffs, robots taking control of the planet, a post-truth world. Which of these comes to mind first when we talk about the disruptive innovation AI brings?

In her NeurIPS talk, “Are We Having the Wrong Nightmares About AI?”, Zeynep Tufekci argues that societies systematically misread the impact of major technological transformations in their early stages. We prepare for risks we already understand, like generals organizing for the previous war, while missing the challenges that actually matter.

A growing theme in the research community is that AI’s intelligence is categorically different from human intelligence. That directly challenges the mental model of linear AI progress – the assumption that AI will “grow up” as a person does. 

Not less capable, but differently capable

LLMs can beat humans on many benchmarks and tests, yet they still struggle with basic tasks beyond their generative capabilities. A simple physical task makes this gap tangible. 

The image below illustrates the simple task of how to open a glass beer bottle with a metal house key; the model fails, despite the task being familiar and rather straightforward for most people. 

Prompt: You need to open a glass bottle of beer, but you don’t have a bottle opener handy. However, you have a metal house key. Illustrate how to use the key to open the bottle. Model: gpt-image-1.5.

This contrast shows a recurring pattern. LLMs can perform well above a professional level on some structured, text-based problems, then fail on others that even children can handle with ease. The issue is not a question of capability; rather, it is a matter of frame of reference. These systems do not sit higher or lower on a human scale. They operate on a different scale altogether.

AI as another form of intelligence

In 2025, a widespread view was that AI in its current form represents another kind of intelligence, one that cannot be directly projected onto a human talent scale. Despite rapid progress, an old research goal remains unresolved: How do we help large language models to perform the kinds of practical tasks that every human can?

LLMs can match or outperform humans in structured, text-based evaluations, yet they continue to lag behind when it comes to working out genuinely novel solutions and adapting when faced with complex, non-stationary settings.

Several researchers argue that this gap reflects a deeper mismatch between human and model learning. Zeynep Tufekci stresses that generative AI is not a form of human intelligence, while Blake Lemoine puts it more bluntly: “Only one thing is clear: LLMs are not human.”

Studies comparing children and models show that young children can infer causal structures from only a handful of observations. In contrast, large language models struggle to identify the relevant causal relationships at all.

Other experiments demonstrate that in more complex, non-stationary environments, LLMs fail to match human adaptability, particularly when effective directed exploration matters.

Strong evaluations don’t translate into impact (yet?)

This disconnect may help explain what Ilya Sutskever described as one of the confusing aspects of current models. They perform extremely well on evaluations, yet the economic impact trails far behind. 

Strong benchmark results do not translate directly into robust performance in open-ended, real-world settings.

In a software development context, this has direct implications. We should not align LLMs with humans, neither in the requirements we impose on development processes nor in the outputs we expect them to produce. 

As we involve LLMs more deeply, with their distinct strengths and limitations, the surrounding processes will need to change accordingly. Effective use will come less from forcing models into human-shaped roles and more from reshaping workflows to fit the kind of intelligence they actually provide.

LLMs will transform ecosystems

When we talk about technology ecosystems, we often focus on tools. Biological ecosystems remind us that this view is incomplete. An ecosystem includes not only the organisms, but also the environment they live in, and that environment is neither static nor passive. 

Organisms actively shape it, and in doing so, they create conditions that favour their own survival and reproduction, while sometimes destroying environments that no longer serve them.

Software development has followed a similar pattern. Codebases, programming languages, build systems, and deployment practices have repeatedly reshaped not only the code itself, but also collaboration and development processes. These elements form the environment in which development tools operate, and they co-evolve with those tools.

Given the pace of LLM adoption, we should expect a comparable shift. LLMs are unlikely to remain passive inhabitants of today’s development environment. Instead, the ecosystem itself will change to better suit their strengths. 

Languages, best practices, and workflows will emerge, evolve, or disappear based on how compatible they are with an LLM-dominated environment and how effectively they enable AI-driven work.

The sweetness of the bitter lesson

Richard Sutton, one of the pioneers of reinforcement learning, formulated what he called the “bitter lesson” after decades of AI research. 

His observation was that many apparent breakthroughs come from injecting human knowledge into systems, for example, by hand-crafting rules, heuristics, or domain-specific structures. 

These approaches often deliver quick wins. Over time, however, they tend to lose to more general methods that rely on learning and search capabilities, and that scale with increases in computation and data.

Sutton’s point was not that human knowledge is useless, but that it becomes a limiting factor. Systems built around general methods continue to improve as computing grows, while systems constrained by human-designed shortcuts eventually hit a ceiling.

Applied to software development, the implication is significant. If we treat development processes, tools, and workflows as methods, then approaches that maximize effective AI utilization are likely to win over time. 

In contrast, approaches that restrict AI involvement or introduce friction, including heavy human-in-the-loop dependencies, risk becoming bottlenecks as models and infrastructure continue to scale.

A view towards the future

Predictions in 2026 are hard. Still, the research points in a consistent direction. LLMs are a different beast, and we should stop treating them as junior humans who will replace us one by one. 

They will reshape the software development environment to suit their particular kind of intelligence. 

Alongside incremental improvements to today’s workflows, we should explore more radical shifts, deliberately reshaping codebases and processes to maximize effective AI utilization.

Scaling the Open VSX Registry responsibly with rate limiting

Scaling the Open VSX Registry responsibly with rate limiting

The Open VSX Registry has become widely used infrastructure for modern developer tools. That growth reflects strong trust from the ecosystem, and it brings a shared responsibility to keep the Registry reliable, predictable, and equitable for everyone who depends on it.

In a previous post, I shared an update on strengthening supply-chain security in the Open VSX Registry, including the introduction of pre-publish checks for extensions. This post focuses on the operational side of the same goal: ensuring the Registry remains resilient and sustainable as usage continues to grow.

The Open VSX Registry is free to use, but not free to operate

Operating a global extension registry requires sustained investment in:

  • Compute and storage to serve and index extensions at scale
  • Bandwidth to deliver downloads and metadata worldwide
  • Security to protect users, publishers, and the service itself
  • Staff to operate, monitor, secure, and support the Registry

These costs scale directly with usage.

AI-driven usage is scaling faster than ever

Demand on the Open VSX Registry is increasing rapidly, and AI-enabled development is accelerating that trend. A single developer can now orchestrate dozens of agents and automated workflows, generating traffic that previously would have required entire teams. In practical terms, that can mean the equivalent load of twenty or more traditional users, with direct impact on compute, bandwidth, storage, security capacity, and operational oversight.

This is not unique to the Open VSX Registry. It is an industry-wide challenge. Stewards of public package registries such as Maven Central, PyPI, crates.io, and Packagist have recently raised the same sustainability concerns in a joint statement on sustainable stewardship. Mike Milinkovich, Executive Director of the Eclipse Foundation, echoed that message in his post on aligning responsibility with usage.

As reliance on shared open infrastructure grows, sustaining it becomes a collective responsibility across the ecosystem.

Open VSX is critical, and often invisible, infrastructure

Many developers and organisations may not realise how often they rely on the Open VSX Registry. It provides the extension infrastructure behind a growing number of modern developer platforms and tools, including Amazon’s Kiro, Cursor, Google Antigravity, Windsurf, VSCodium, IBM’s Project Bob, Trae, Ona (formerly Gitpod), and others.

If you use one of these tools, you use the Open VSX Registry.

The Open VSX Registry remains a neutral, vendor-independent public service, operated in the open and governed by the Eclipse Foundation for the benefit of the entire ecosystem.

For developers, the expectation is simple: Open VSX should remain fast, stable, secure, and dependable as the ecosystem grows.

As more platforms and automated systems rely on the Registry, continuous machine-driven traffic can place sustained load on shared infrastructure. Without clear operational guardrails, that can affect performance and availability for everyone.

A practical step for sustainable and reliable operations

Usage has shifted from primarily human-driven access to continuous automation driven by CI systems, cloud-based tooling, and AI-enabled workflows. That shift requires operational controls that scale predictably.

Rate limiting provides a structured way to manage high-volume automated traffic while preserving the performance developers expect. It also ensures that operational decisions are based on real usage patterns and that expectations for large-scale consumption are clear and transparent.

Rate limits aren’t entirely new. Like most public infrastructure services, the Open VSX Registry has long had baseline protections in place to prevent sustained high-volume usage from degrading performance for everyone. What’s changing now is that we’re moving from a one-size-fits-all approach to defined tiers that more accurately reflect different usage patterns. This allows us to keep the Registry stable and responsive for developers and open source projects, while providing a clear, supported path for sustained at-scale consumption.

For individual developers and open source projects, day-to-day workflows remain unchanged. Publishing extensions, searching the registry, and installing tools will continue to work as they always have for typical usage.

A measured, transparent rollout

Rate limiting will be introduced incrementally, with an emphasis on platform health and operational stability.

The initial phase focuses on visibility and observation before any limits are adjusted. This includes improved insight into traffic patterns for registered consumers, baseline protections for anonymous high-volume usage, and a monitoring period before any limits are adjusted.

This work is being done in the open so the community can follow what is changing and why. Progress and discussion are tracked publicly in the Open VSX deployment issue:
https://github.com/EclipseFdn/open-vsx.org/issues/5970

What this means for the community

The goal is to keep the Open VSX Registry reliable and fair as it scales, while minimizing impact on normal use.

For most users, nothing should feel different. Developers should see little to no impact, and publishers should not experience disruption to normal publishing workflows. Sustained, high-volume automated consumers may need to coordinate with the Registry to ensure their usage can be supported reliably over time.

Organisations that depend on the Open VSX Registry for sustained or commercial-scale usage are encouraged to get in touch. Coordinating early helps us plan capacity, maintain reliability, and support the broader ecosystem. Please contact the Open VSX Registry team at infrastructure@eclipse-foundation.org.

The intent is not restriction, but clarity in support of fairness, stability, and long-term sustainability.

Looking ahead

Automation is reshaping how developer infrastructure is consumed. Responsible rate limiting is one step toward ensuring the Open VSX Registry can continue to serve the ecosystem reliably as those patterns evolve.

We will continue to adapt based on real-world usage and community input, with the goal of keeping the Open VSX Registry a dependable shared resource for the long term.

Christopher Guindon


Designing For Agentic AI: Practical UX Patterns For Control, Consent, And Accountability

In the first part of this series, we established the fundamental shift from generative to agentic artificial intelligence. We explored why this leap from suggesting to acting demands a new psychological and methodological toolkit for UX researchers, product managers, and leaders. We defined a taxonomy of agentic behaviors, from suggesting to acting autonomously, outlined the essential research methods, defined the risks of agentic sludge, and established the accountability metrics required to navigate this new territory. We covered the what and the why.

Now, we move from the foundational to the functional. This article provides the how: the concrete design patterns, operational frameworks, and organizational practices essential for building agentic systems that are not only powerful but also transparent, controllable, and worthy of user trust. If our research is the diagnostic tool, these patterns are the treatment plan. They are the practical mechanisms through which we can give users a palpable sense of control, even as we grant AI unprecedented autonomy. The goal is to create an experience where autonomy feels like a privilege granted by the user, not a right seized by the system.

Core UX Patterns For Agentic Systems

Designing for agentic AI is designing for a relationship. This relationship, like any successful partnership, must be built on clear communication, mutual understanding, and established boundaries.

To manage the shift from suggestion to action, we utilize six patterns that follow the functional lifecycle of an agentic interaction:

  • Pre-Action (Establishing Intent)
    The Intent Preview and Autonomy Dial ensure the user defines the plan and the agent’s boundaries before anything happens.
  • In-Action (Providing Context)
    The Explainable Rationale and Confidence Signal maintain transparency while the agent works, showing the “why” and “how certain.”
  • Post-Action (Safety and Recovery)
    The Action Audit & Undo and Escalation Pathway provide a safety net for errors or high-ambiguity moments.

Below, we will cover each pattern in detail, including recommendations for metrics for success. These targets are representative benchmarks based on industry standards; adjust them based on your specific domain risk.

1. The Intent Preview: Clarifying the What and How

This pattern is the conversational equivalent of saying, “Here’s what I’m about to do. Are you okay with that?” It’s the foundational moment of seeking consent in the user-agent relationship.

Before an agent takes any significant action, the user must have a clear, unambiguous understanding of what is about to happen. The Intent Preview, or Plan Summary, establishes informed consent. It is the conversational pause before action, transforming a black box of autonomous processes into a transparent, reviewable plan.

Psychological Underpinning
Presenting a plan before action reduces cognitive load and eliminates surprise, giving users a moment to verify the agent truly understands their intent.

Anatomy of an Effective Intent Preview:

  • Clarity and Conciseness
    The preview must be immediately digestible. It should summarize the primary actions and outcomes in plain language, avoiding technical jargon. For instance, instead of “Executing API call to cancel_booking(id: 4A7B),” it should state, “Cancel flight AA123 to San Francisco.”
  • Sequential Steps
    For multi-step operations, the preview should outline the key phases. This reveals the agent’s logic and allows users to spot potential issues in the proposed sequence.
  • Clear User Actions
    The preview is a decision point, not just a notification. It must be accompanied by a clear set of choices. It’s a moment of intentional friction, a ‘speed bump’ in the process designed to ensure the user is making a conscious choice, particularly for irreversible or high-stakes actions.

Let’s revisit our travel assistant scenario from the first part of this series. We use this proactive assistant to illustrate how an agent handles a flight cancellation. The agent has detected a flight cancellation and has formulated a recovery plan.

The Intent Preview would look something like this:

Proposed Plan for Your Trip Disruption

I’ve detected that your 10:05 AM flight has been canceled. Here’s what I plan to do:

  1. Cancel Flight UA456
    Process refund and confirm cancellation details.
  2. Rebook on Flight DL789
    Book a confirmed seat on a 2:30 PM non-stop flight, as this is the next available non-stop flight with a confirmed seat.
  3. Update Hotel Reservation
    Notify the Marriott that you will be arriving late.
  4. Email Updated Itinerary
    Send the new flight and hotel details to you and your assistant, Jane Doe.

[ Proceed with this Plan ] [ Edit Plan ] [ Handle it Myself ]

This preview is effective because it provides a complete picture, from cancellation to communication, and offers three distinct paths forward: full consent (Proceed), a desire for modification (Edit Plan), or a full override (Handle it Myself). This multifaceted control is the bedrock of trust.

When to Prioritize This Pattern
This pattern is non-negotiable for any action that is irreversible (e.g., deleting user data), involves a financial transaction of any amount, shares information with other people or systems, or makes a significant change that a user cannot easily undo.

Risk of Omission
Without this, users feel ambushed by the agent’s actions and will disable the feature to regain control.

Metrics for Success:

  • Acceptance Ratio
    Plans Accepted Without Edit / Total Plans Displayed. Target > 85%.
  • Override Frequency
    Total Handle it Myself Clicks / Total Plans Displayed. A rate > 10% triggers a model review.
  • Recall Accuracy
    Percentage of test participants who can correctly list the plan’s steps 10 seconds after the preview is hidden.

Applying This to High-Stakes Domains

While travel plans are a relatable baseline, this pattern becomes indispensable in complex, high-stakes environments where an error results in more than an inconvenience for an individual traveling. Many of us work in settings where wrong decisions may result in a system outage, putting a patient’s safety at risk, or numerous other catastrophic outcomes that unreliable technology would introduce.

Consider a DevOps Release Agent tasked with managing cloud infrastructure. In this context, the Intent Preview acts as a safety barrier against accidental downtime.

In this interface, the specific terminology (Drain Traffic, Rollback) replaces generalities, and the actions are binary and impactful. The user authorizes a major operational shift based on the agent’s logic, rather than approving a suggestion.

2. The Autonomy Dial: Calibrating Trust With Progressive Authorization

Every healthy relationship has boundaries. The Autonomy Dial is how the user establishes it with their agent, defining what they are comfortable with the agent handling on its own.

Trust is not a binary switch; it’s a spectrum. A user might trust an agent to handle low-stakes tasks autonomously but demand full confirmation for high-stakes decisions. The Autonomy Dial, a form of progressive authorization, allows users to set their preferred level of agent independence, making them active participants in defining the relationship.

Psychological Underpinning
Allowing users to tune the agent’s autonomy grants them a locus of control, letting them match the system’s behavior to their personal risk tolerance.

Implementation
This can be implemented as a simple, clear setting within the application, ideally on a per-task-type basis. Using the taxonomy from our first article, the settings could be:

  • Observe & Suggest
    I want to be notified of opportunities or issues, but the agent will never propose a plan.
  • Plan & Propose
    The agent can create plans, but I must review every one before any action is taken.
  • Act with Confirmation
    For familiar tasks, the agent can prepare actions, and I will give a final go/no-go confirmation.
  • Act Autonomously
    For pre-approved tasks (e.g., disputing charges under $50), the agent can act independently and notify me after the fact.

An email assistant, for example, could have a separate autonomy dial for scheduling meetings versus sending emails on the user’s behalf. This granularity is key, as it reflects the nuanced reality of a user’s trust.

When to Prioritize This Pattern
Prioritize this in systems where tasks vary widely in risk and personal preference (e.g., financial management tools, communication platforms). It is essential for onboarding, allowing users to start with low autonomy and increase it as their confidence grows.

Risk of Omission
Without this, users who experience a single failure will abandon the agent completely rather than simply dialing back its permissions.

Metrics for Success:

  • Trust Density
    Percentage breakdown of users per setting (e.g., 20% Suggest, 50% Confirm, 30% Auto).
  • Setting Churn
    Number of Setting Changes / Total Active Users per month. High churn indicates trust volatility.

3. The Explainable Rationale: Answering Why?

After taking an action, a good partner explains their reasoning. This pattern is the open communication that follows an action, answering Why? before it’s even asked. “I did that because you’ve told me in the past that you prefer X.”

When an agent acts, especially autonomously, the immediate question in the user’s mind is often, Why did it do that? The Explainable Rationale pattern proactively answers this question, providing a concise justification for the agent’s decisions. This is not a technical log file. In my first article of this series, we discussed translating system primitives into user-facing language to prevent deception. This pattern is the practical application of that principle. It transforms the raw logic into a human-readable explanation grounded in the user’s own stated preferences and prior inputs.

Psychological Underpinning
When an agent’s actions are explainable, they feel logical rather than random, helping the user build an accurate mental model of how the agent thinks.

Effective Rationales:

  • Grounded in Precedent
    The best explanations link back to a rule, preference, or prior action.
  • Simple and Direct
    Avoid complex conditional logic. Use a simple “Because you said X, I did Y” structure.

Returning to the travel example, after the flight is rebooked autonomously, the user might see this in their notification feed:

I’ve rebooked your canceled flight.

  • New Flight: Delta 789, departing at 2:30 PM.
  • Why I took this action:
    • Your original flight was canceled by the airline.
    • You’ve pre-approved autonomous rebooking for same-day, non-stop flights.

[ View New Itinerary ] [ Undo this Action ]

The rationale is clear, defensible, and reinforces the idea that the agent is operating within the boundaries the user established.

When to Prioritize This Pattern
Prioritize it for any autonomous action where the reasoning isn’t immediately obvious from the context, especially for actions that happen in the background or are triggered by an external event (like the flight cancellation example).

Risk of Omission
Without this, users interpret valid autonomous actions as random behavior or ‘bugs,’ preventing them from forming a correct mental model.

Metrics for Success:

  • Why? Ticket Volume
    Number of support tickets tagged “Agent Behavior — Unclear” per 1,000 active users.
  • Rationale Validation
    Percentage of users who rate the explanation as ‘Helpful’ in post-interaction microsurveys.

4. The Confidence Signal

This pattern is about the agent being self-aware in the relationship. By communicating its own confidence, it helps the user decide when to trust its judgment and when to apply more scrutiny.

To help users calibrate their own trust, the agent should surface its own confidence in its plans and actions. This makes the agent’s internal state more legible and helps the user decide when to scrutinize a decision more closely.

Psychological Underpinning
Surfacing uncertainty helps prevent automation bias, encouraging users to scrutinize low-confidence plans rather than blindly accepting them.

Implementation:

  • Confidence Score
    A simple percentage (e.g., Confidence: 95%) can be a quick, scannable indicator.
  • Scope Declaration
    A clear statement of the agent’s area of expertise (e.g., Scope: Travel bookings only) helps manage user expectations and prevents them from asking the agent to perform tasks it’s not designed for.
  • Visual Cues
    A green checkmark can denote high confidence, while a yellow question mark can indicate uncertainty, prompting the user to review more carefully.

When to Prioritize This Pattern
Prioritize when the agent’s performance can vary significantly based on the quality of input data or the ambiguity of the task. It is especially valuable in expert systems (e.g., medical aids, code assistants) where a human must critically evaluate the AI’s output.

Risk of Omission
Without this, users will fall victim to automation bias, blindly accepting low-confidence hallucinations, or anxiously double-check high-confidence work.

Metrics for Success:

  • Calibration Score
    Pearson correlation between Model Confidence Score and User Acceptance Rate. Target > 0.8.
  • Scrutiny Delta
    Difference between the average review time of low-confidence plans and high-confidence plans. Expected to be positive (e.g., +12 seconds).

5. The Action Audit & Undo: The Ultimate Safety Net

Trust requires knowing you can recover from a mistake. The Undo function is the ultimate relationship safety net, assuring the user that even if the agent misunderstands, the consequences are not catastrophic.

The single most powerful mechanism for building user confidence is the ability to easily reverse an agent’s action. A persistent, easy-to-read Action Audit log, with a prominent Undo button for every possible action, is the ultimate safety net. It dramatically lowers the perceived risk of granting autonomy.

Psychological Underpinning
Knowing that a mistake can be easily undone creates psychological safety, encouraging users to delegate tasks without fear of irreversible consequences.

Design Best Practices:

  • Timeline View
    A chronological log of all agent-initiated actions is the most intuitive format.
  • Clear Status Indicators
    Show whether an action was successful, is in progress, or has been undone.
  • Time-Limited Undos
    For actions that become irreversible after a certain point (e.g., a non-refundable booking), the UI must clearly communicate this time window (e.g., Undo available for 15 minutes). This transparency about the system’s limitations is just as important as the undo capability itself. Being honest about when an action becomes permanent builds trust.

When to Prioritize This Pattern
This is a foundational pattern that should be implemented in nearly all agentic systems. It is absolutely non-negotiable when introducing autonomous features or when the cost of an error (financial, social, or data-related) is high.

Risk of Omission
Without this, one error permanently destroys trust, as users realize they have no safety net.

Metrics for Success:

  • Reversion Rate
    Undone Actions / Total Actions Performed. If the Reversion Rate > 5% for a specific task, disable automation for that task.
  • Safety Net Conversion
    Percentage of users who upgrade to Act Autonomously within 7 days of successfully using Undo.

6. The Escalation Pathway: Handling Uncertainty Gracefully

A smart partner knows when to ask for help instead of guessing. This pattern allows the agent to handle ambiguity gracefully by escalating to the user, demonstrating a humility that builds, rather than erodes, trust.

Even the most advanced agent will encounter situations where it is uncertain about the user’s intent or the best course of action. How it handles this uncertainty is a defining moment. A well-designed agent doesn’t guess; it escalates.

Psychological Underpinning
When an agent acknowledges its limits rather than guessing, it builds trust by respecting the user’s authority in ambiguous situations.

Escalation Patterns Include:

  • Requesting Clarification
    “You mentioned ‘next Tuesday.’ Do you mean September 30th or October 7th?”
  • Presenting Options
    “I found three flights that match your criteria. Which one looks best to you?”
  • Requesting Human Intervention
    For high-stakes or highly ambiguous tasks, the agent should have a clear pathway to loop in a human expert or support agent. The prompt might be: “This transaction seems unusual, and I’m not confident about how to proceed. Would you like me to flag this for a human agent to review?”

When to Prioritize This Pattern
Prioritize in domains where user intent can be ambiguous or highly context-dependent (e.g., natural language interactions, complex data queries). Use this whenever the agent operates with incomplete information or when multiple correct paths exist.

Risk of Omission
Without this, the agent will eventually make a confident, catastrophic guess that alienates the user.

Metrics for Success:

  • Escalation Frequency
    Agent Requests for Help / Total Tasks. Healthy range: 5-15%.
  • Recovery Success Rate
    Tasks Completed Post-Escalation / Total Escalations. Target > 90%.
Pattern Best For Primary Risk Key Metric
Intent Preview Irreversible or financial actions User feels ambushed >85% Acceptance Rate
Autonomy Dial Tasks with variable risk levels Total feature abandonment Setting Churn
Explainable Rationale Background or autonomous tasks User perceives bugs “Why?” Ticket Volume
Confidence Signal Expert or high-stakes systems Automation bias Scrutiny Delta
Action Audit & Undo All agentic systems Permanent loss of trust <5% Reversion Rate
Escalation Pathway Ambiguous user intent Confident, catastrophic guesses >90% Recovery Success

Table 1: Summary of Agentic AI UX patterns. Remember to adjust the metrics based on your specific domain risk and needs.

Designing for Repair and Redress

This is learning how to apologize effectively. A good apology acknowledges the mistake, fixes the damage, and promises to learn from it.

Errors are not a possibility; they are an inevitability.

The long-term success of an agentic system depends less on its ability to be perfect and more on its ability to recover gracefully when it fails. A robust framework for repair and redress is a core feature, not an afterthought.

Empathic Apologies and Clear Remediation

When an agent makes a mistake, the error message is the apology. It must be designed with psychological precision. This moment is a critical opportunity to demonstrate accountability. From a service design perspective, this is where companies can use the service recovery paradox: the phenomenon where a customer who experiences a service failure, followed by a successful and empathetic recovery, can actually become more loyal than a customer who never experienced a failure at all. A well-handled mistake can be a more powerful trust-building event than a long history of flawless execution.

The key is treating the error as a relationship rupture that needs to be mended. This involves:

  • Acknowledge the Error
    The message should state clearly and simply that a mistake was made.
    Example: I incorrectly transferred funds.
  • State the Immediate Correction
    Immediately follow up with the remedial action.
    Example: I have reversed the action, and the funds have been returned to your account.
  • Provide a Path for Further Help
    Always offer a clear link to human support. This de-escalates frustration and shows that there is a system of accountability beyond the agent itself.

A well-designed repair UI might look like this:

We made a mistake on your recent transfer.
I apologize. I transferred $250 to the wrong account.

✔ Corrective Action: The transfer has been reversed, and your $250 has been refunded.
✔ Next Steps: The incident has been flagged for internal review to prevent it from happening again.

Need further help? [ Contact Support ]

Building the Governance Engine for Safe Innovation

The design patterns described above are the user-facing controls, but they cannot function effectively without a robust internal support structure. This is not about creating bureaucratic hurdles; it is about building a strategic advantage. An organization with a mature governance framework can ship more ambitious agentic features with greater speed and confidence, knowing that the necessary guardrails are in place to mitigate brand risk. This governance engine turns safety from a checklist into a competitive asset.

This engine should function as a formal governance body, an Agentic AI Ethics Council, comprising a cross-functional alliance of UX, Product, and Engineering, with vital support from Legal, Compliance, and Support. In smaller organizations, these ‘Council’ roles often collapse into a single triad of Product, Engineering, and Design leads.

A Checklist for Governance

  • Legal/Compliance
    This team is the first line of defense, ensuring the agent’s potential actions stay within regulatory and legal boundaries. They help define the hard no-go zones for autonomous action.
  • Product
    The product manager is the steward of the agent’s purpose. They define and monitor its operational boundaries through a formal autonomy policy that documents what the agent is and is not allowed to do. They own the Agent Risk Register.
  • UX Research
    This team is the voice of the user’s trust and anxiety. They are responsible for a recurring process for running trust calibration studies, simulated misbehavior tests, and qualitative interviews to understand the user’s evolving mental model of the agent.
  • Engineering
    This team builds the technical underpinnings of trust. They must architect the system for robust logging, one-click undo functionality, and the hooks needed to generate clear, explainable rationales.
  • Support
    These teams are on the front lines of failure. They must be trained and equipped to handle incidents caused by agent errors, and they must have a direct feedback loop to the Ethics Council to report on real-world failure patterns.

This governance structure should maintain a set of living documents, including an Agent Risk Register that proactively identifies potential failure modes, Action Audit Logs that are regularly reviewed, and the formal Autonomy Policy Documentation.

Where to Start: A Phased Approach for Product Leaders

For product managers and executives, integrating agentic AI can feel like a monumental task. The key is to approach it not as a single launch, but as a phased journey of building both technical capability and user trust in parallel. This roadmap allows your organization to learn and adapt, ensuring each step is built on a solid foundation.

Phase 1: Foundational Safety (Suggest & Propose)

The initial goal is to build the bedrock of trust without taking significant autonomous risks. In this phase, the agent’s power is limited to analysis and suggestion.

  • Implement a rock-solid Intent Preview: This is your core interaction model. Get users comfortable with the idea of the agent formulating plans, while keeping the user in full control of execution.
  • Build the Action Audit & Undo infrastructure: Even if the agent isn’t acting autonomously yet, build the technical scaffolding for logging and reversal. This prepares your system for the future and builds user confidence that a safety net exists.

Phase 2: Calibrated Autonomy (Act with Confirmation)

Once users are comfortable with the agent’s proposals, you can begin to introduce low-risk autonomy. This phase is about teaching users how the agent thinks and letting them set their own pace.

  • Introduce the Autonomy Dial with limited settings: Start by allowing users to grant the agent the power to Act with Confirmation.
  • Deploy the Explainable Rationale: For every action the agent prepares, provide a clear explanation. This demystifies the agent’s logic and reinforces that it is operating based on the user’s own preferences.

Phase 3: Proactive Delegation (Act Autonomously)

This is the final step, taken only after you have clear data from the previous phases demonstrating that users trust the system.

  • Enable Act Autonomously for specific, pre-approved tasks: Use the data from Phase 2 (e.g., high Proceed rates, low Undo rates) to identify the first set of low-risk tasks that can be fully automated.
  • Monitor and Iterate: The launch of autonomous features is not the end, but the beginning of a continuous cycle of monitoring performance, gathering user feedback, and refining the agent’s scope and behavior based on real-world data.

Design As The Ultimate Safety Lever

The emergence of agentic AI represents a new frontier in human-computer interaction. It promises a future where technology can proactively reduce our burdens and streamline our lives. But this power comes with profound responsibility.

Autonomy is an output of a technical system, but trustworthiness is an output of a design process. Our challenge is to ensure that the user experience is not a casualty of technical capability but its primary beneficiary.

As UX professionals, product managers, and leaders, our role is to act as the stewards of that trust. By implementing clear design patterns for control and consent, designing thoughtful pathways for repair, and building robust governance frameworks, we create the essential safety levers that make agentic AI viable. We are not just designing interfaces; we are architecting relationships. The future of AI’s utility and acceptance rests on our ability to design these complex systems with wisdom, foresight, and a deep-seated respect for the user’s ultimate authority.