Author Archives: DevegygiebyOL

Why most “AI in Developer Tools” breaks outside the demo

Why most “AI in Developer Tools” breaks outside the demo

AI-assisted development is no longer judged by how impressive it looks in a demo. For teams working on real systems, the question is whether AI still holds up once it meets real workflows, real constraints, and real delivery pressure.

That question will be answered at OCX by Jonas Helming. As Principal Software Architect at EclipseSource and a long-time contributor to the Eclipse Foundation ecosystem, he works on AI-native developer tools such as Eclipse Theia, helping teams adopt AI coding in ways that are transparent, extensible, and usable in practice. Rather than treating AI as a feature layered onto existing tools, his focus is on how AI reshapes developer workflows end to end.

In an interview ahead of OCX, Helming explains why many AI demos fail to translate into daily engineering practice. Too often, teams treat AI as something they can simply bolt onto existing processes. He says, “if you consider AI as something that you want to add to your existing workflow, you create a blocker from day one.” AI-assisted development does not sit neatly alongside established practices. It reshapes them entirely, from how developers work day to day to how projects and requirements are managed.

This disconnect becomes obvious in live environments. AI can dramatically reduce the effort required to execute well-defined tasks, but it also shifts pressure elsewhere. In the interview, Helming explains that “the surrounding workflow becomes the bottleneck and requires more attention, basically because the execution is shrinking.” Preparation, problem framing, quality control, and deployment suddenly matter more than ever, and they are exactly where many demos stop short.

 

AI in action at OCX

This is why he insists on live demonstrations in his OCX session, AI in Action: The Ultimate Live Demo with Theia AI. Rather than slides, he shows AI agents being created, customised, and orchestrated in real time inside an AI-native IDE. The session shows both where AI delivers immediate efficiency and where careful engineering decisions are still required.

If you are sceptical of AI demos that only work on stage, this session is designed for you. Helming’s live, hands-on approach shows what already works today and what still breaks in open, extensible developer tools. 

Join him in Brussels in April 2026 at Open Community Experience 2026 to see what AI in developer tools actually delivers today, where it still requires careful engineering, and what that means for teams building serious software.

Image
OCX

Daniela Nastase


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