MATLAB, a programming and numeric computation environment for engineers and scientists is sinking in popularity, according to the latest edition of the Tiobe index of programming language popularity.
In the Tiobe index for August 2026, published August 8, MATLAB is ranked 25th, having fallen out of the top 20 for the first time in more than 10 years. “Is this just a temporary dip, or is it the beginning of a more permanent decline? Personally, I believe MATLAB’s days are numbered,” said Paul Jansen, CEO at Tiobe. One important reason for the decline may be MATLAB’s licensing model. “In a world where most programming languages are freely available, having to pay for a MATLAB license is becoming an increasingly significant disadvantage,” Jansen said.
Another reason for MATLAB’s decline is that the language has not evolved as much as other mainstream programming languages, according to Tiobe. “While languages such as Python, C++,Rust, and even newer languages continue to introduce modern language features and programming paradigms, MATLAB can increasingly feel like a language from another era,” Jansen said.
The Tiobe Programming Community index is an indicator of the popularity of programming languages. Ratings are based on the number of skilled engineers world-wide, courses, and third-party vendors. Popular websites including Google, Amazon, Wikipedia, Bing, and more than 20 others are used to calculate the ratings.
Python 3.15 is one of the most feature-packed Python releases in many a moon, and the first release candidate has just arrived. Here’s a rundown of the biggest, boldest, and most important innovations, changes, and fixes.
Lazy imports
A long-asked for feature, lazy imports allow imports to be processed only when they’re actually used by the program. For slow-importing modules that impose a large cost on a program’s startup time, you can defer that cost to when the code of that module will actually be executed.
You can use lazy imports explicitly using the new lazy import syntax, but you can also force code with conventional imports to behave lazily, either programmatically or by using an environment variable. This makes it easy to make existing code take advantage of this feature without tons of rewriting. Best of all, there’s no drawback to making imports lazy: they otherwise behave exactly as intended.
The frozendict built-in type
Only rarely does Python add a new data type, but this is a long-debated and long-desired addition: the frozen dictionary. The frozendict behaves like a regular dictionary, except that it’s immutable (you can’t add, remove, or change elements) and it’s hashable (so you can use it as a key in another dictionary, for instance).
The sentinel() built-in type
Another new addition to the language is intended to replace a common and problematic Python pattern: creating a unique sentinel object (as an alternative to None where None could be a valid value, for example) by using object(). The new syntax ,sentinel("NAME"), creates unique objects that compare only to themselves via the is operator. These objects can be type-checked properly, and they have an informative representation instead of just a random object descriptor.
Tachyon, a statistical sampling profiler
The long-standing cProfile module profiles Python code deterministically—that is, it tracks and records every single call. That makes it precise, but it also means a cProfile-tracked program runs far slower than normal. A new profiling module in Python 3.15, profiling.sampling, uses statistical sampling methods to garner useful information about performance at a fraction of the impact on the program’s speed. The existing cProfile profiler is still available—it’s not going away—but has a new alternate name, profiling.tracing.
An upgraded JIT
CPython’s built-in just-in-time (JIT) compiler debuted in Python 3.13. Its long-term goals are to make Python programs run faster without any changes to code, in something of the same way the alternate Python runtime PyPy can speed things up. And it comes without the cost of changing to a totally different interpreter with some of its own limitations.
The first couple of revisions of the JIT didn’t promise, or deliver, a great deal of additional speed, as they were more about laying a foundation for future improvements. With Python 3.15, though, the JIT is now showing an 8% to 13% geometric mean performance improvement over standard CPython, depending on the platform and workload. The biggest changes include a new tracing front end (to enable more speedups on more kinds of code), the use of register allocation for faster and more memory-efficient work, better machine code generated by the JIT, and additional optimizations such as eliminating reference counts for some classes of objects.
It’s worth experimenting with enabling the JIT for workloads to see if there’s any measurable difference. But note that further development on the JIT is now subject to some firmly laid-out guidelines regarding the performance improvement it needs to provide before it can be considered a fully supported part of Python, and not just an experimental sideline.
Better error messages
Error messages in Python have been made more precise, detailed, and useful over the last couple of versions, and Python 3.15 continues that work. The highlights:
Suggestions for missing names (“x has no attribute ‘y‘. Did you mean ‘xyz‘?”) now include suggestions from the members of a given object, and not just the object itself.
Suggestions now also cover checks for deleting attributes, not just accessing them.
If the interpreter can’t come up with a suggestion for a method based on fuzzy name matching via Levenshtein distance, it consults a list of names commonly used in other languages for such methods. For example, if you attempt to use list.push() (a JavaScript method), the interpreter suggests .append(), the proper method for Python lists.
Type system improvements
The TypedDict class, which lets you create dictionaries with predefined keys and type-hinted keys and values, adds support for two new arguments in its definition. The closed argument lets you specify if only the keys specified can be used at runtime. The extra_items argument lets you specify additional keys at runtime, but only keys with a value of a specified type.
The TypeForm type definition lets you represent the value that results from evaluating a type expression. With this, type annotations can be used in places where the type itself is being used as a value—for instance, variations on operations like typing.cast or even isinstance, or as part of how a third-party type-checking tool works.
Unpacking in comprehensions
This is another long-requested feature. If you wanted to completely unpack or “flatten” a nested object using a comprehension, you used to need a function like itertools.chain() or you would have to write a nested comprehension with an ugly syntax:
x = [[1,2,3],[4,5],[6]]
y = [a for b in x for a in b]
>>> [1, 2, 3, 4, 5, 6] # y
Unpacking in comprehensions using the star operator lets you save yourself a step:
x = [[1,2,3],[4,5],[6]]
y = [*a for a in x]
>>> [1, 2, 3, 4, 5, 6] # y
Unpacking with ** also works, for instance as a way to flatten and combine dictionaries:
dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
y = {**d for d in dicts}
>>> {'a': 3, 'b': 2}
Finally, this kind of unpacking can also be used to form generator expressions:
(*x for x in ["ab","cd","ef"])
The expression above creates a generator that yields:
['a', 'b', 'c', 'd', 'e', 'f']
Reverting the incremental garbage collector
Finally, Python 3.15 takes an important U-turn. Python 3.14 featured a major change to its garbage collection system—an incremental garbage collector intended to reduce the amount of program-stopping time needed to collect garbage. Unfortunately, many users reported the new garbage collector increases process memory usage, sometimes dramatically. Python 3.15 will revert back to the older generational garbage collector used in Python 3.13 and before. The incremental collector may return in a future version, but not without additional work done on it to keep this problem from resurfacing.
Other changes
A new math module, math.integer, gives you functions for integer-specific math such as greatest common divisor or integer square root.
The Stable ABI was created to make it easier to write CPython extensions that didn’t have to be recompiled to be compatible with multiple point revisions of Python. With Python 3.15, developers writing extensions that use the free-threaded (aka “no-GIL”) build of Python can now use the Stable ABI. This will require some rewriting of extensions that use the Stable ABI; it isn’t something you can accomplish simply by recompiling code with different headers.
CPython builds now have frame pointers enabled by default where supported. This makes it easier and faster for the stack to unwind, so that CPython can be analyzed more reliably by system-level profiling and debugging tools.
UTF-8 is now the default encoding for Python globally. You no longer need to specify UTF-8 as an encoding when, for instance, reading text from files. You can disable this behavior using a command-line flag or environment variable.
The Rust team has announced that it is enabling the Polonius Alpha borrow checker on nightly releases for testing. The team expects to fully stabilize Polonius Alpha later in the year.
The announcement was made in a blog post on August 4. In the blog post, Rust team member Jack Huey said there are no known remaining issues with Polonius Alpha and performance is generally acceptable for stabilization.
Enabling the Polonius Alpha borrow checker on nightly for testing is being done to help find serious performance regressions, unsoundness in the formulation, and diagnostic issues that need to be improved, Huey said. Issues can be reported in GitHub or Zulip.
Developers can disable Polonius Alpha and use the stable NLL borrow checker by passing -Zpolonius=off to rustc , using RUSTFLAGS=-Zpolonius=off, or including the following in a project’s .cargo/config.toml configuration file:
The Polonius borrow checker has been in the works since 2018. In 2023, a new formulation of a the Polonius borrow checker was proposed that required a minimal re-architecture of the existing NLL (non-lexical lifetime) implementation and could be extended to allow more sound code to compile, according to Huey. Stabilization of that implementation has been delayed until now.
The borrow checker is a component of the Rust compiler that enforces strict rules on references, or borrowing. These include ensuring that all variables are initialized before they are used, that the same value isn’t moved twice, that a value isn’t moved while it is borrowed, that a place isn’t accessed while it is mutably borrowed (except through the reference), and that a place isn’t mutated while it is immutably borrowed, according to the Rust documentation.
Java Development Kit (JDK) 28, a non-LTS (Long-Term Support) or “feature release” of standard Java due in March 2027, has started to take shape. Features listed for JDK 28 now include a preview of value objects, switching the default mode of the Shenandoah garbage collector to generational mode, and a preview of strict field initialization in the Java Virtual Machine (JVM).
As a non-LTS release, JDK 28 will be backed by six months of support by Oracle.
The three features currently targeted to JDK 28 include the following:
Introduce value objects, which are immutable and lack object identity. Value objects are distinguished by the values of their fields, and can be represented by the JVM in ways that improve performance. A goal of the feature is to give developers a programming model for immutable data in which the == operator, and all other operations, distinguish objects by the values of their fields rather than their identities. Value objects is a preview language and VM feature.
Switch the default mode of the Shenandoah Garbage Collector (GC) to the generational mode and deprecate the non-generational mode, with the intent to remove it in a future release. Goals include signaling the intent that future development will focus on generational mode, and reducing the maintenance cost of supporting two different modes. However, it is not a goal to remove non-generational mode at this time.
Introduce strictly-initialized fields in the JVM. Such fields must be initialized before they are read, thus default values such as 0 or null are never observed. For strictly-initialized fields that are final, the same value is always observed. A goal is offering designers of JVM-based programming languages a model for field initialization, which has stronger integrity guarantees than the present model. This is a preview VM feature.
The predecessor to JDK 28, JDK 27, is due September 15. JDK 27 also is a non-LTS release that will be backed by six months of support by Oracle.
KotlinLLM, a research prototype for delegating runtime logic to a large language model (LLM) from Kotlin code, is now going open source and public, JetBrains announced.
Revealed July 28, KotlinLLM is an IntelliJ IDEA plugin prototype for experimenting with LLM-driven “Smart macros” in Kotlin, enabling code generation, runtime updates, and hot-reloading. In software engineering, LLMs are commonly used during development for code completion, code generation, and program comprehension, JetBrains noted, but using an LLM at run time of a compiled application is much less common. The existing options for doing this have the following trade-offs, according to JetBrains:
Direct runtime delegation is slow, non-deterministic, and costly, and makes the application depend on an LLM service at run time.
External agent workflowskeep the generated logic outside the codebase, where it is harder to review, test, and ship.
Most prior work (byLLM, nightjar, Healer) targets interpreted languages like Python, not a compiled, statically typed language like Kotlin.
KotlinLLM addresses these limitations in three ways, JetBrains said:
The call site shows that a feature is LLM-backed, so it is visible in code review.
Generated behavior is saved as an ordinary Kotlin source, not kept only in the runtime session. It can be committed, reviewed, tested, and distributed like any other code.
Once generated, the code runs as plain Kotlin without the plugin. For scenarios that are already covered, there is no further LLM call, so no added latency or cost, and the result is reproducible.
KotlinLLM is open source under Apache License 2.0. The repository contains the KotlinLLM IntelliJ IDEA plugin, the Smart macro API, and example projects.
Unveiled July 28, MCP C# SDK v2.0 implements the 2026-07-28 revision of the MCP specification, which Microsoft described as the protocol’s largest revision since its launch. The 2026-07-28 revision makes the MCP protocol stateless by default, standardizes the HTTP surface to allow ordinary HTTP infrastructure to route MCP traffic, and introduces Multi Round-Trip Requests (MRTR) to support interaction without requiring long-lived sessions.
MCP C# SDK 2.0 moves in lockstep with these changes, Microsoft said. The HTTP server transport now runs statelessly by default. Whereas version 1.x of the SDK configured stateful sessions out of the box, in version 2.0 HttpServerTransportOptions.Stateless defaults to true.
MCP C# SDK v2.0 also implements a small set of HTTP headers that the 2026-07-28 MCP revision standardizes. A tools/call now includes Mcp-Method: tools/call and Mcp-Name: get_order_status alongside the JSON-RPC body. With the 2026-07-28 revision, an MCP request becomes a single, self-describing HTTP POST. This means that ordinary HTTP infrastructure such as load balancers, proxies, gateways, and web application firewalls now can act on MCP traffic without deep packet inspection, Microsoft said.
And MCP C# SDK 2.0 adds support for MRTR on the server and on the client. MRTR addresses scenarios in which tools can’t answer in a single round trip, such as when a server wants to confirm with a user before taking a significant action. In previous versions of MCP, these interactions worked only over a live, stateful session. With MRTR, they work with no session at all, because all the continuity travels in the payload, Microsoft said.
With the 2026-07-28 spec compliance landing in MCP C# SDK 2.0, Microsoft said the SDK’s next focus is end-to-end authentication and authorization. The goal is to make secure MCP deployments as “turnkey” as the rest of the stack, building on the closer OAuth and OpenID Connect alignment the spec introduced, Microsoft said.
Java Development Kit 27, an Oracle-driven, planned update to standard Java, is set to reach its initial release candidate (RC) stage on August 6. This update has features ranging from making the Garbage-First (G1) garbage collector the default collector and adding structured concurrency to simplify concurrent programming. JDK 27 is currently in rampdown phase 2, with the feature set now frozen.
Overall, there are nine features listed on what is described as a reference implementation of JDK 27. The first five features include making G1 the default garbage collector, post-quantum hybrid key exchange for TLS 1.3, lazy constants, structured concurrency, and primitive types in patterns, instanceof, and switch. The next four include compact object headers, JFR (JDK Flight Recorder) in-process data redaction, a vector API, and PEM (Privacy-Enhanced Mail) encodings for cryptographic objects.
The final release candidate stage for JDK 27 follows on August 20, with general availability scheduled for September 15. Early-access builds for JDK 27 can be found at jdk.java.net.
With JDK 27, G1 becomes the default garbage collector in all environments, rather than just in server environments. Goals are to ensure that the HotSpot JVM will always select G1 and that performance including throughput, latency, memory footprint, and startup time does not degrade significantly.
With post-quantum hybrid key exchange for TLS 1.3, the goal is to enhance the security of Java applications that require secure network communication by implementing hybrid key exchange algorithms. Such algorithms defend against future quantum computing attacks by combining a quantum-resistant algorithm with a traditional algorithm.
Lazy constants introduces an API for objects that hold unmodifiable data. Lazy constants are treated as true constants by the JVM, enabling the same performance optimizations that are enabled by declaring a field final. Compared to final fields, however, lazy constants offer greater flexibility in the timing of their initialization. This feature is in a third preview.
The goal of primitive types in patterns, instanceof, and switch is to enhance pattern matching by allowing primitive types in all pattern contexts, and to extend instanceofand switch to work with all primitive types. This feature is in a fifth preview.
Structured concurrency simplifies concurrent programming by introducing an API that treats groups of related tasks running in different threads as single units of work. Goals include streamlining error handling and cancellation, improving reliability, and enhancing observability. Structured concurrency is in its seventh preview.
JDK 27 makes compact object headers the default object header layout in the HotSpot JVM. Compact object headers reduce object headers from 96 bits down to 64 bits on 64-bit architectures, reducing heap size, improving deployment density, and increasing data locality.
JFR data in-process redaction allows users to redact command-line arguments and the initial values of environment variables and system properties in JFR recordings. JFR recording files may contain events that include sensitive data, such as secrets in command-line arguments, access tokens in environment variables, and passwords in system properties. This data is (optionally) redacted before it leaves the process, so that sensitive information does not leak.
The vector API introduces an API to express vector computations that reliably compile at run time to optimal vector instructions on supported CPUs, thus achieving better performance than equivalent scalar computations. This is an incubating API in its 12th incubation.
PEM encodings of cryptographic objects introduces an API for encoding objects that represent cryptographic keys, certificates, and certificate revocation lists in the PEM transport format, and for decoding from PEM back into objects. This is a preview API.
JDK 27 is a short-term feature release (non-LTS) that will receive six months of support from Oracle. Its predecessor, JDK 26, also a short-term release, became generally available on March 17.
For a programming language that is prized for being convenient and easy, Python has some surprising barbs and sharp edges. Find out how a new pip flag can make it easier to work with project dependencies apart from the project itself. Get the skinny on why standalone Python programs are hard to assemble, and what you can do about it. See how Python’s new Installation Manager for Microsoft Windows lets multiple versions coexist on the same system. And find out what will be shipped when Python 3.15 debuts later this year.
Top picks for Python readers on InfoWorld
New pip flag fixes longstanding Python frustration A longstanding gripe with Python’s pip package manager has a happy ending. The next version of pip will let you install a package’s requirements without installing the package itself.
Why it’s so hard to create stand-alone Python apps Python’s best features, its dynamism and flexibility, have a downside: They make it tough to package Python apps as standalone programs. Not impossible, though; just … complicated.
The best new features in Python 3.15 If you haven’t grabbed the beta release of Python 3.15 yet, it’s ready when you are. Here’s a rundown of all the best bits you can start using right away.
Get started with the new Python Installation Manager This all-in-one Python version management tool is now the recommended, default way to set up and manage Python versions on Microsoft Windows — including betas, release versions, and the special JIT and free-threaded builds, too.
More good reads and Python updates elsewhere
Ruff linter gets a massive upgrade in version 0.16 The speedy, Rust-powered linter and formatter for Python now supports over 400 rules by default, making for a more useful, zero-configuration, out-of-the-box linting experience.
What every Python developer should know about the CPython ABI A massive, multilayered unpacking of that most intimidating of Python internals, the CPython application binary interface. Even if you never plan to write a CPython extension or contribute to the CPython project, the details laid out here are eye-opening.
Free-threaded Python: past, present, and future At PyCon 2026, CPython core developer Thomas Wouters gave a talk about how Python came to support threads via the GIL, and how its developers went about removing the GIL over many years and multiple attempts. Excellent recap available at LWN.net.
PEP 837: Extensible JSON serialization A newly introduced Python Enhancement Proposal (intended for Python 3.16) to make Python’s native JSON serialization more extensible, for instance to allow new types to automatically support serialization via a __json__ method.
Over the past year, I had the same conversation with almost every team starting an AI initiative. Should we build on Semantic Kernel, AutoGen or Foundry?
At first it felt like the most important architectural decision we’d make. Each framework had its own philosophy, each promised to be the foundation for enterprise AI, and picking the wrong one felt like an expensive mistake. I spent a lot of time helping teams weigh the trade-offs.
Looking back, I think we were asking the wrong question. I certainly was.
I watched teams spend months debating SDKs while the decisions that actually decided whether their applications survived production went unexamined. Some built elaborate orchestration layers for workflows that a few deterministic functions would have handled. Others avoided agent frameworks entirely and later found they’d designed themselves into a corner.
Then Microsoft settled it for us. It introduced the unified Agent Framework, quietly moved Semantic Kernel and AutoGen into maintenance mode, and the debate I’d spent months refereeing was suddenly over. Turns out the answer to “which of the three” was “none of the three, here’s a fourth.” The framework hit version 1.0 and general availability in April 2026, stable across .NET and Python.
What surprised me wasn’t the decision. It was how fast a debate that had eaten so much of our attention stopped mattering. Microsoft changed the menu.
It didn’t change the meal.
The framework was never the hard part
Framework selection dominated almost every early conversation I had about enterprise agents. Which SDK do we standardize on? Which orchestration model gives us the most flexibility? Which one is Microsoft actually betting on?
Fair questions. But after a year of watching these projects play out, I’ve slowly come around to a different view. Those weren’t the questions that decided anything.
The first question I ask now is much smaller. Does this thing actually need an agent?
It sounds obvious, and I still get it wrong sometimes. But it’s the mistake I see most. On one project, a team spent weeks designing a multi-agent workflow for a process that ran the same four steps every time: read a document, validate it, call an API, send a notification. The diagrams looked great. The system in production didn’t. A few well-tested functions would have been easier to build, easier to maintain and a lot easier to trust.
Part of this is just that “agent” has become the word everyone reaches for. Sometimes it’s the right call. Sometimes it’s a workflow we already knew how to build, wearing a newer label. An agent earns its complexity when it genuinely has to decide things you can’t predetermine, choosing between tools, adapting to what it finds, working out its own next step. If you already know every step, you have a workflow, and a workflow is usually the better engineering choice. The consolidation didn’t change that. It just made it easier to see.
What building production agents actually taught me
Once I stopped fixating on frameworks, the same three problems kept showing up. None of them had anything to do with the SDK.
Context beats model choice
Early on I spent a lot of time comparing models, the way you’d agonize over a restaurant menu and then order what you always order. Now I spend most of it thinking about context, which is far less fun and far more useful.
I’ve watched good models fail because they were handed too much, not too little. One team I worked with gave the model access to nearly every internal document they had on the theory that more information meant better answers. It went the other way. Responses got slower, less consistent and sometimes skipped right past the thing that actually mattered. When we cut the context down to only what the task needed, the quality jumped almost immediately. I didn’t predict that. It taught me to be suspicious of “just give it everything.”
The best agent systems I’ve worked on weren’t the ones with the biggest context windows. They were the ones careful about what reached the model, and when. That’s not something the framework hands you.
Failure is where the real work is
Most agent demos look great because they’re built around the happy path. Production doesn’t extend that courtesy.
I remember a project where everything held up in testing. Then a downstream API timed out after the agent had already completed several earlier steps. We couldn’t just restart, because part of the business process had already gone through. We ended up spending far more time on recovery logic than we ever spent on prompts. That project changed how I think about this work. The hard part was never getting the model to make a decision. It was making sure the system didn’t fall apart when reality refused to follow the script.
Tool calls fail partway through. APIs return inconsistent data. Models call the same tool over and over because the last answer wasn’t what they wanted. That’s not the exception; that’s a normal Tuesday. Whether you retry, roll back, pause for a human or push on with partial results is a judgment call, and no framework is going to make it for you.
Identity is the real security boundary
This one surprised me most. The moment an agent stops being a chatbot and starts touching real business systems, identity matters more than orchestration.
Every project gets to the same question eventually. Who is this agent actually acting as? The developer’s credentials? A service account? The user who asked? Get it wrong and you’ve built something autonomous running with more access than any single person should have, which is exactly the kind of thing that looks fine until an audit. The Agent Framework, like most modern tooling, makes it easier to wire agents to tools through standards like the Model Context Protocol. That helps. But where human approval belongs, what needs extra authorization, how much rope to give the thing, those are still yours to decide.
The surprises weren’t technical
Here’s what I didn’t see coming. The hardest part of last year wasn’t technical at all. It was organizational. The moment a team heard “agent,” expectations shifted under everyone’s feet. Business stakeholders started expecting full autonomy. Developers assumed the thing could reason its way through anything. People started designing for flexibility before we’d even agreed on what problem we were solving. The word did damage before any code did. I found myself spending as much time resetting expectations as I did discussing architecture.
Build for change, not for today’s winner
I don’t think the teams that struggled last year picked the wrong framework. Semantic Kernel was reasonable. AutoGen was reasonable. Foundry made sense for plenty of cases. I’d have signed off on any of them.
The ones that got hurt put all their eggs in one framework, treating it as the foundation of the whole system instead of as one more dependency. Microsoft provided a migration path. But teams that had tightly coupled their applications to framework-specific abstractions discovered that migrating and rewriting are not the same thing. That wasn’t Microsoft’s doing. It was their own architecture’s. The teams that moved easily had kept their business logic, prompts and orchestration loose enough to evolve independently of any one SDK. For them, the change was a manageable project, not a teardown.
For what it’s worth, nobody I work with is treating this as an emergency. Most are moving the smaller workloads first, watching how they behave and leaving the production-critical systems alone until they actually understand the new abstractions. That’s the right instinct. And I doubt this is the last consolidation we’ll see, the ecosystem is still young, frameworks will keep absorbing each other and over time the differences between them will be operational more than architectural.
I don’t regret the framework debates, honestly. They were reasonable at the time. What changed wasn’t Microsoft’s roadmap.
It was mine. Watching these systems run in production taught me that the framework is the easiest piece to swap out. Recovery logic, context management, security boundaries, the business workflow itself, those stay with you long after today’s SDK gets replaced by tomorrow’s.
So, Microsoft made one decision easier by turning three frameworks into one. Good. Five years from now we’ll be on different tools, and we’ll still be asking the same handful of questions.
Does this actually need an agent? Does it have the right context? Can it recover when something breaks, because something will? Is it acting as the right person?
Those questions outlast every rewrite. That’s where I’ve learned to put my effort.
Frameworks come and go. Good architecture has to survive all of them.
This article is published as part of the Foundry Expert Contributor Network. Want to join?
EF Core is Microsoft’s flagship ORM (object-relational mapper), the software layer that allows .NET developers to work with relational databases. The DbContext class is the core component of the EF Core framework for managing database operations. However, the DbContext class in EF Core is not thread-safe. Hence, if you share DbContext instances between multiple threads, you will often encounter data corruption issues and the InvalidOperationException.
In this article, we’ll learn how we can execute queries in parallel in EF Core by handling thread-safety issues to avoid concurrency errors. To work with the code examples provided in this article, you should have Visual Studio 2026 installed in your system. You can download Visual Studio 2026 here.
Executing EF Core queries in parallel – the problem
When working in today’s data-driven applications, you will often need to fetch data from multiple unrelated datasets. In applications that use concurrency, thread-safety is critical to guaranteeing correct execution, avoiding data corruption and race conditions, and ensuring data consistency. Let’s understand this with an example.
Let’s say we want to populate a dashboard that displays all recently processed orders, metrics, logs, and traces, as well as your application’s performance metadata. We might write the following code.
public class Dashboard
{
public List Orders { get; set; } = new();
public Metrics Metrics { get; set; } = new();
public List Logs { get; set; } = new();
public List Traces { get; set; } = new();
}
public static async Task LoadDashboardAsync(ProductService productService)
{
Task<List> ordersTask = productService.GetProcessedOrdersAsync();
Task metricsTask = productService.GetMetricsAsync();
Task<List> logsTask = productService.GetRecentLogsAsync();
Task<List> tracesTask = productService.GetTracesAsync();
await Task.WhenAll(ordersTask, metricsTask, logsTask, tracesTask);
return new Dashboard
{
Orders = await ordersTask,
Metrics = await metricsTask,
Logs = await logsTask,
Traces = await tracesTask
};
}
In the preceding code snippet, there are four read operations that are executed by four different Task instances. Our objective is to ensure that the database round trips run in parallel instead of in sequence. We can accomplish this by usingTask.WhenAll, which starts the four tasks, waits for every task to finish, then returns the data wrapped inside a new Dashboard instance.
If we executed these queries sequentially, the user would have to wait until each query completed its execution in turn—for a total wait time equal to the sum of the times for all four queries. However, by running these queries in parallel, we reduce the wait time considerably. The user will need to wait only as long as it takes for the slowest of the four queries to complete its execution.
However, there is a danger with the above approach. If you run multiple operations on the same DbContext instance, you will see an InvalidOperationException with the following message:
A second operation started in this context before the previous operation was completed. This is usually caused by multiple threads using the same DbContext instance; instance members are not guaranteed to be thread-safe.
Databases such as SQL Server, PostgreSQL, and Oracle Database follow a request-response communication model at the connection level: a single connection can process only one command at a time. Hence, you cannot run multiple queries concurrently using the connection. If you await several operations using the same connection, EF Core detects the overlapping use of a non-thread-safe context and throws an InvalidOperationException. To run queries in parallel, you must give each task its own connection or context.
Why DbContext isn’t thread-safe – and how to work around it
The DbContext class in EF Core is designed to manage a single unit of work. To be more precise, EF Core does not provide support for running multiple operations on the same DbContext instance. This design approach creates inherent challenges when you use the same DbContext instance across multiple threads. If DbContext were thread-safe, extensive locking would be required, which would degrade data access performance.
This stateful design of DbContext makes it unsuitable for concurrent access patterns that involve loading, modifying, or tracking different sets of data simultaneously, because it needs to maintain the internal representation of database state.
The change tracker is one of the most important components of DbContext in EF Core. It monitors all entities loaded into memory and detects any changes made to them after they have been loaded. It keeps track of the original, current, and changed values of the entities, thereby enabling the EF Core runtime to know the current state of these entities when you call the SaveChanges() method on the DbContext instance.
To implement thread-safety when working with DbContext, we must write our code to ensure that each concurrent operation gets its own copy of a short-lived instance. Now, we could accomplish this by wrapping a shared DbContext instance inside a thread-safe block using the lock keyword, so that all calls to the database take place using one and only one thread at a time. This approach is illustrated in the code snippet below.
using Microsoft.EntityFrameworkCore;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options) { }
public DbSet Products => Set();
}
However, while the above approach gives us the thread-safety we need, it can degrade data access performance considerably. A better approach is to use IDbContextFactory , which creates fresh DbContext instances on demand. Calling its CreateDbContext() method is cheap and produces a fresh, isolated context every time.
The following code snippet shows how you can register an instance of type IDbContextFactory as a singleton. You can safely call this code from any thread.
Executing EF Core queries in parallel – the solution
Now let’s see how we can put IDbContextFactory to work. The following code illustrates a class named ProductService that uses a factory to create DbContext instances for each scope of work.
public class ProductService
{
private readonly IDbContextFactory _factory;
public ProductService(IDbContextFactory factory)
=> _factory = factory;
public async Task GetByIdAsync(int id)
{
await using var context = await _factory.CreateDbContextAsync();
return await context.Products.FindAsync(id);
}
public async Task UpdateStockQuantityAsync(int id, int updateQuantity)
{
await using var context = await _factory.CreateDbContextAsync();
var product = await context.Products.FindAsync(id);
if (product is null) return;
product.Quantity += updateQuantity;
await context.SaveChangesAsync();
}
}
Note that ProductService has two methods, GetByIdAsync and UpdateStockQuantityAsync. An instance of the DbContext class is created locally in each of these methods. Now, suppose you have two threads, T1 and T2, that execute these methods concurrently. That is, thread T1 executes the GetByIdAsync method while thread T2 executes the UpdateStockQuantityAsync method. Because each of these methods is executed in isolation, they will have their own context, connection, and change-tracking information, and there will be no mutable state, so you don’t need to implement thread synchronization in either of these methods.
Consider the following code that executes a read operation and an update operation in two separate tasks.
The Task.WhenAll method runs the two tasks in parallel and waits until both have finished. The reason this approach is thread-safe, and will not create concurrency errors, is that each of these two methods creates its own DbContext instance internally. Therefore the read operation and the update operation use independent DbContext instances.
Use DbContext pooling to reduce allocation cost
Although creating DbContext instances is not that costly, you should consider using pooled contexts in applications that require high scalability and high performance. The following code snippet shows how you can register a pooled context.
A call to AddDbContext() will register a DbContext instance as scoped per HTTP request. Each request will run on a different thread and each will have its own context. However, keep in mind that the default scoped registration of the DbContext will not always suffice.
You will need a factory to create instances of DbContext when you’re using a background service, or performing some work inside a particular request, or running some business logic operation over multiple contexts.
Key takeaways
If you use EF Core in the data access layer of your application, you must implement thread safety measures whenever you run your queries in parallel.
You cannot execute multiple queries in parallel in EF Core using the same DbContext instance.
The IDbContextFactory enables you to create a DbContext instance for each thread, thereby enabling you to work with these instances in isolation.
Although using a DbContext pool involves a small allocation overhead, it becomes a non-issue if you need high throughput.
A new version of Python’s native package management tool, pip, will remove a limitation that has frustrated Python developers for years. If you wanted to install the dependencies for a given package, but not install the package itself, you were stuck. Either you had to extract the dependency list from the package and install it by hand, or you had to build the whole package anyway.
Why was this a problem? Sometimes, you want only the dependencies for a package—for instance, as a way to create a separate environment for testing or another project. If you’re making source distributions via CI, some requirements might be needed to make the source distribution but aren’t actually included in it (e.g., Cython). You would need to install these requirements somewhere—apart from the project itself—to perform the build step.
A dependencies-only install mode for packages is a long-requested feature. Developer James O’Claire found many examples of such requests, along with various workarounds. Most of those involved third-party solutions of some sort.
Now, a new feature set to land in pip version 26.2 will fix this problem from the inside. The command pip install --only-deps will install only the dependencies for a given package. Note that it will not install build dependencies for the package, but only runtime dependencies.
Note that you can accomplish this with existing third-party tools. For instance, uv sync --no-install-project has the same behavior. But having this functionality right inside pip means you don’t have to turn to external tooling—or ugly hacks—to solve the problem. This may be an example of how third-party projects like uv (a superfast Python package installer) are inspiring native Python features where they make sense.
pip 26.2 is scheduled to be released by the end of July 2026.
G# (GSharp)is moving forward as a programming language for Microsoft’s .NET platform, touted as bringing Go-, Kotlin-, and Swift-style ergonomics to the CLR (Common Language Runtime). The language is described by its creators as modern, simple, and accessible.
Although pre-1.0 and still growing, G# aims to be for people who want a small, predictable language with direct access to the .NET ecosystem. Developers will see imports, func, structs, slices, maps, channels, go, select, and for in iteration. Also important are nullable flow, direct calls into the CLR (Common Language Runtime), and built-in concurrency.
With G#, copyrighted in 2026, developers get value-oriented structs, reference-oriented classes, data structs, and data classes. For concurrency, G# uses scope for structured concurrency, async funcand awaitfor task-based asynchrony, and async sequence[T] for asynchronous streams. G# also makes use of the same Task and Task[T] types familiar from the .NET BCL (Base Class Library).
G# documentation is available on the GitHub site of Microsoft software engineer David Obando. “Every .NET type—your packages, third-party NuGet packages, the BCL—is callable from G# with the syntax you already know. CLR generics use G#’s bracket spelling, and method calls, properties, indexers, and for inover IEnumerable[T] all just work,” according to the website.
A Visual Studio Code extension for G3 can be found at marketplace.visualstudio.com. The extension adds syntax highlighting, language server features, build/run commands, and debugger configuration for .gs and .gsproj files. Developers can install the extension from within VS Code (search for “G#” in the Extensions view) or from the command line.
The evolution of Java is the subject of a just-released documentary about the programming language and development platform. “The Java Story: The Official Documentary” tells the story of Java through interviews with the engineers who created it and shepherded it through three decades.
Produced by CultRepo and sponsored by Oracle, JetBrains, IBM, and Azul, the documentary follows Java from its set-top box and browser-based origins at Sun Microsystems in the 1990s and through its rise to dominate server-side computing in the 2000s, the “dark ages” and resurgence with Java 8 under Oracle in the 2010s, and its continuing modernization and promising role in AI today. “From its humble beginnings as a project code-named ‘Oak’ at Sun Microsystems to becoming a global standard for enterprise software and billions of devices, Java’s journey is one of radical innovation, strategic pivots, and enduring community strength,” said Cult.Repo.
The documentary also delves into Sun’s bitter Java licensing dispute with Microsoft, Oracle’s suit of Google over its use of Java APIs Android (Google won), the creation of the Java Community Process, Sun’s open-sourcing of Java, and Oracle’s switch to the six-month release cycle. Technical enhancements such as lambda expressions in Java 8, virtual threads in Java 21 (Project Loom), and the ongoing refactor to bring value objects to the Java object model (Project Valhalla) also get attention.
Technical experts and other Java figures interviewed in the documentary include James Gosling, creator of Java; Kim Polese, Java’s first product manager; Carla Schroer, director of Java compatibility at Sun Microsystems; James Duncan Davidson, creator of Apache Tomcat; Mark Reinhold, chief architect of the Java Platform Group at Oracle; Brian Goetz, Java language architect in the Java Platform Group at Oracle; Rod Johnson, creator of Spring; and Gavin King, creator of Hibernate.
Preview 6 of Microsoft’s .NET 11 software development platform is now available, with improvements across the runtime, libraries, frameworks, and C# and F# programming languages.
Announced July 14, .NET 11 Preview 6 can be downloaded from dotnet.microsoft.com. The full, general production release is expected in November. The first preview was unveiled February 10.
Runtime-async, the .NET runtime’s built-in implementation of async/await, keeps getting faster in Preview 6, building on the suspension improvements from Preview 5. Async continuations can now opt out of ExecutionContext capture and restore. And the JIT now compiles a dedicated runtime-async version of a synchronous, task-returning method rather than delegating to it through a thunk. The JIT turns the method’s tail calls into runtime-async calls and awaits the task that would otherwise have been returned, so the async path no longer pays for an extra layer of indirection, Microsoft said.
In library improvements, System.Text.Json can now serialize and deserialize the new C# union types. The serializer recognizes a union through the new JsonTypeInfoKind.Union contract type, reads and writes the active case, and supports the reflection-based serializer and the source generator. The new JsonUnionAttribute, JsonUnionCaseInfo, and type-classifier APIs enable customization of how cases are discovered and named.
In C# improvements, extension members now include indexers, so developers can add this[...] access to a type from an extension block. This rounds out the extension members feature introduced in .NET 10, which already supported extension methods and properties. An extension indexer is declared like an instance indexer inside an extension block.
Other highlights in .NET 11 Preview 6:
Math.BigMul(long, long, out long) is now significantly faster on x64. The JIT generates a single MUL r/m64 instruction when both operands are 64-bit values and the caller requests the high half of the result. Previously the JIT emitted a helper call. The change eliminates the call overhead and makes code that multiplies large numbers tighter.
Minimal API validation in ASP.NET Core now supports asynchronous validators end-to-end.
For F#, the compiler emits better sequence points for several common debugging scenarios. Breakpoints and stepping are improved for call arguments that need stack-empty debug points, for expressions and comprehensions, simple literal bindings, and if or match conditions.
For .NET MAUI, .NET 11 Preview 6 advances the cross-platform CollectionView and Shell architecture, removes the legacy Xamarin.Forms compatibility package, and brings the Apple workloads onto stable Xcode 26.6.
Also for .NET MAUI, HybridWebView JavaScript interop is reworked to use a source generator instead of reflection, so it no longer produces trim or AOT (ahead of time) compiler warnings and works correctly in fully trimmed and Native AOT apps.
Projects that use Microsoft.TypeScript.MSBuild in Razor Class Libraries now properly integrate TypeScript compilation outputs with ASP.NET Core Static Web Assets.
With this new codebase, TypeScript 7.0 brings native code speed, shared memory multithreading, and a number of new optimizations that typically yield speedups between 8x and 12x on full builds, Microsoft said in a July 8 announcement. This port of TypeScript to native code was done as faithfully as possible, writing new code while maintaining the structure and logic of the original codebase to keep results consistent and compatible between the two compilers, according to Microsoft.
TypeScript 7.0 is available via npm:
npm install -D typescript
To help with the TypeScript 6.0 to TypeScript 7.0 transition process, Microsoft has published a new compatibility package, @typescript/typescript6. This package provides an executable named tsc6, so that if needed, a developer can install TypeScript 7.0 (which ships its own tsc binary) side-by-side with TypeScript 6.0 without naming conflicts. The new package also re-exports the TypeScript 6.0 API, so that a developer can use tsc for TypeScript 7, while other tooling can continue to rely on TypeScript 6.0.
TypeScript 7.0 is a major milestone in the TypeScript project, Microsoft said. This port has been the primary focus for Microsoft’s team for more than a year. With TypeScript 7.0 now out, the team will return to new feature work, ergonomic improvements, more performance wins, and implementing a new API for the broader ecosystem. Microsoft expects a fairly similar timeline to releases prior to TypeScript 7.0, with new versions published every three to four months.
Meta’s long-awaited Pyrefly linter is out in a 1.0 version, and the forthcoming Python 3.15 has a super-efficient sampling profiler. Plus we have a comprehensive rundown of Python’s indispensable virtual environments — and a warning about a novel breed of malware that exploits Python’s package ecosystem.
Top picks for Python readers on InfoWorld
How to use virtual environments in Python Isolate and protect your Python projects from each other, and empower them to do more, with virtual environments and their native-to-Python tooling.
Pyrefly 1.0: A fast, forward-looking Python linter The first full release of Meta’s long-awaited linting and type checking tool for Python delivers speed and offers advanced features for type-checking PyTorch and Django projects.
Hands-on with the new sampling profiler in Python 3.15 Among Python 3.15’s best new features is a sampling profiler, for instrumenting your code and finding its bottlenecks with a minimum of performance impact or fuss. See up-close how it works.
Python Steering Council calls for temporary pause on JIT project The requested pause stays in place until a proper Standards Track PEP lands for the experimental JIT (just-in-time) compiler, the better to describe how the JIT will be a formal and supported part of Python.
Pyodide 314.0: Pyodide packages on PyPI Thanks to PEP 783, Python packages built with Pyodide (Python ported to WebAssembly) can be installed straight from PyPI instead of through Pyodide — another step closer to Py-on-Wasm becoming an everyday thing.
All about that Python 3.14 garbage collection rigmarole A new garbage collector introduced in Python 3.14 was yanked at the last minute due to reports of higher memory usage. Here’s a deep dive into what changed for the worse and why.
Are you really expected to run five type checkers now? No, but you should keep your options open. This blog post from a Pyrefly contributor recommends choosing one of the major offerings (Mypy, Pyrefly, Pyright, ty, Zuban, etc.), but also getting to know the others too.
Understanding a reactivity model in the abstract is useful, but it is ultimately incomplete without seeing how it shapes real application code. Concepts such as state, derivation, and explicit dependencies only become meaningful when they influence how forms are built, validated, and maintained in practice.
This article focuses on a concrete example: a modest but realistic registration form. Rather than introducing new concepts, the goal here is to make earlier ideas tangible. We will see how a signal-backed model reshapes validation, interaction state, and submission logic, and how much coordination logic simply disappears when form behavior is expressed declaratively.
The focus here is not on novelty or completeness, but on making the underlying ideas easier to reason about. By walking through a signal-first form from model definition to submission, we can evaluate whether this approach truly reduces complexity and where it introduces new trade-offs that teams should understand before adopting it more broadly.
With the conceptual groundwork in place, we can now turn theory into a concrete implementation. In this section, we will build a fully working registration form using Angular’s Signal Forms API. This example is deliberately modest in scope, but it is designed to serve as the foundation for the rest of the series. Each subsequent article will extend this same example rather than introducing a new one.
The form collects an email address, a password, a confirmation password, and explicit acceptance of terms. While simple on the surface, this structure allows us to explore field-level validation, cross-field constraints, interaction state, and submission behavior, all without reverting to event-driven form logic.
Project setup and structure
The example assumes a standard Angular application created with the Angular CLI and configured to use Signals (Angular 17+). The Signal Forms APIs (Angular 21+) live under @angular/forms/signals, which must be explicitly imported.
Separating the model from the component keeps form state independent of presentation. This becomes increasingly valuable as the form grows or is reused across multiple components.
Defining the form model
We begin by defining the shape of the data that the form collects. This is a plain TypeScript interface with no Angular dependencies. Treating the form model as a simple data structure reinforces the idea that the form’s values are just state.
This interface mirrors what would typically be sent to a back-end API. There is no duplication of state, no separate “form value” object, and no mapping required at submission time.
Creating the signal-backed form
The form itself is created in the component using a writable signal as the source of truth. The form() function attaches form semantics validation, field state, and submission to that signal.
// registration.component.ts
import { CommonModule } from "@angular/common";
import { Component, signal } from "@angular/core";
import {
email,
form,
FormField,
required,
submit,
} from "@angular/forms/signals";
import { RegistrationData } from "./registration.model";
@Component({
selector: "app-registration",
imports: [FormField, CommonModule],
templateUrl: "./registration.html",
styleUrl: "./registration.css",
})
export class Registration {
readonly model = signal({
email: "",
password: "",
confirmPassword: "",
acceptedTerms: false,
});
readonly registrationForm = form(this.model, (schema) => {
required(schema.email, { message: "Email is required" });
email(schema.email, { message: "Enter a valid email address" });
required(schema.password, { message: "Password is required" });
required(schema.confirmPassword, {
message: "Please confirm your password",
});
required(schema.acceptedTerms, {
message: "You must accept the terms to continue",
});
});
async onSubmit(event?: Event) {
event?.preventDefault();
await submit(this.registrationForm, (value) => {
console.log(value());
// Mock Server Call
return Promise.resolve([
{
kind: "EmailAlreadyExists",
field: this.registrationForm.email,
error: { kind: "server", message: "Email already taken" },
},
]);
});
}
}
Several design decisions are worth noting.
First, the model signal is defined as read-only. All mutations to the model occur through form bindings, not ad hoc assignments in the component. This keeps the component declarative and avoids the temptation to manipulate form state imperatively.
Second, validation is declared in one place. The schema function describes constraints on the model without introducing control trees, validator arrays, or observable pipelines. Angular takes responsibility for re-running validation whenever the model changes.
Finally, submission logic is explicit. The submit() helper ensures that the form is valid before invoking the callback, and it passes the current model value directly. There is no need to check flags or manually extract values.
Binding the form to the template
With the form defined, the next step is to bind it to the template. Signal Forms provide the [formField] directive, which connects an input element directly to a field in the form schema.
@if (registrationForm().errors().length > 0) {
@for (error of registrationForm().errors(); track error.message) {
What stands out here is the absence of indirection. Each input binds directly to a field. Validation state is accessed through signals such as invalid() and touched(). Error messages are read from a structured error object, not reconstructed manually.
This template contains no subscriptions, no async pipes, and no event handlers for value changes. The UI simply reflects the current form state.
Interaction state and user experience
One of the common criticisms of declarative form models is that they obscure user interaction logic. Signal Forms address this directly by exposing interaction metadata as signals.
The touched() signal determines whether a field has been interacted with. By combining it with invalid(), we control when validation messages appear. This logic remains purely declarative: the template describes when errors should be visible, and Angular ensures the signals stay up-to-date.
The disabled state of the submit button is derived from registrationForm.invalid(). There is no need to manually enable or disable it in response to events. If the form becomes valid, the button is enabled automatically.
Why this scales
Even at this early stage, several advantages of a signal-first form model are apparent. The form’s behavior is expressed in terms of state and derivation, not events. The model, validation rules, and UI bindings are clearly separated. There is no duplication of logic between the component and the template.
As the form grows, this structure holds. Additional fields introduce additional schema entries and template bindings, not new subscription logic. Cross-field validation can be added declaratively. Asynchronous validation and persistence can be layered on without rewriting the core model.
Most importantly, the form remains inspectable. At any point during execution, the model signal reflects the current state of the form. Derived state validity, errors, and UI flags can be understood by reading the code, not by tracing runtime behavior.
What we did not solve yet (and why)
At this stage, it would be easy to walk away with the impression that Signal Forms eliminates most of the hard problems associated with form handling. That impression would be misleading. What we have built so far is intentionally incomplete, not because the approach falls short, but because introducing too much too early obscures the value of the underlying model.
One area we have deliberately postponed is cross-field validation that expresses richer business rules. Many real-world forms depend on relationships between fields rather than isolated constraints. Password confirmation is a familiar example, but more complex scenarios quickly arise in enterprise applications. While Signal Forms support these patterns, introducing them before establishing a clear understanding of derived state risks turns validation back into an imperative exercise rather than a declarative one.
We have also avoided asynchronous validation. Server-backed checks introduce latency, partial failure, cancellation, and race conditions. These are not trivial concerns, and treating them casually often leads to subtle bugs and confusing user experiences. Although Signal Forms provide the necessary hooks to model asynchronous behavior, doing so responsibly requires a careful discussion of pending state, effects, and life-cycle boundaries. That discussion belongs in its own article.
Another omission is persistence and synchronization. Many forms need to autosave drafts, synchronize state with local storage, or react to changes by triggering external side effects. These behaviors are not part of the form state itself; they are consequences of state changes. Treating them as such is essential to keeping the architecture comprehensible. Introducing persistence too early would blur the distinction between state and reaction that this article has worked to establish.
Finally, this article has not addressed migration and interoperability. Few teams are starting from a blank slate. Most will adopt Signal Forms incrementally within applications that already rely on reactive forms or template-driven forms. Hybrid approaches, bridging strategies, and gradual refactors are all critical topics, but they presuppose familiarity with both paradigms. Addressing migration before establishing a solid signal-first mental model would undermine that foundation.
These omissions are intentional. A form architecture that tries to do everything at once often ends up doing nothing clearly. By focusing on the core ideas of state, derivation, and declarative validation, we create a base that can absorb additional complexity without collapsing under it.
Signal Forms in the context of Angular’s evolution
To fully appreciate Signal Forms, it helps to step back and view them not as an isolated feature, but as part of a broader shift in Angular’s design philosophy.
For much of its history, Angular emphasized declarative templates paired with imperative coordination in component classes. RxJS became the backbone of that coordination, providing a powerful abstraction for handling asynchronous workflows, user input, and external events. This model scaled well, but it also encouraged developers to express state indirectly through streams and subscriptions.
Signals represent a deliberate recalibration. They re-center Angular’s reactivity model around state and derivation, rather than events and emissions. This shift is visible across the framework: in component inputs, change detection, and now forms. Signal Forms are not an attempt to replace everything that came before; they are an attempt to make the most common use case, modeling and deriving state, simpler and more explicit.
Framed this way, the design of Signal Forms aligns more closely with state-driven form behavior. The requirement to start with a model signal reflects the idea that the state should have a single, inspectable source of truth. Schema-based validation aligns with the notion that constraints are properties of state, not behaviors triggered by events. Field state exposed as signals reinforces the idea that validity, errors, and interaction metadata are derived values that should be read, not managed.
It is also worth noting that Signal Forms do not attempt to abstract away form behavior. They do not hide form state behind opaque classes or life-cycle hooks. They do not require developers to think in terms of control hierarchies or subscription graphs. Instead, they expose form behavior directly, making it easier to reason about how values, validation, and UI feedback relate to one another.
This approach aligns closely with other recent changes in Angular, including the introduction of modern template control flow and a stronger emphasis on explicit data dependencies. Together, these features point toward a framework that favors clarity over indirection and composition over orchestration.
Importantly, Signal Forms are still evolving. Their APIs may change, and their surface area will almost certainly expand. That is precisely why grounding them in first principles matters. Developers who understand why Signal Forms work the way they do will be far better equipped to adapt as the APIs mature.
This article has intentionally avoided duplicating documentation or enumerating every available feature. Instead, it has focused on establishing a conceptual framework that makes the official APIs feel intuitive rather than surprising. When viewed this way, Signal Forms are not a new way to write forms; they are a clearer expression of what forms have always been.
A new way to think about forms
Building the registration form in this article reveals a quiet but important shift. The reduction in complexity does not come from fewer features or simpler requirements. It comes from expressing form behavior in terms of state and derivation rather than orchestration and reaction.
By treating the data model as the single source of truth, validation rules as declarative constraints, and UI behavior as derived from current conditions, much of the coordination logic that typically surrounds forms becomes unnecessary. There are fewer subscriptions to manage, fewer flags to synchronize, and fewer life-cycle concerns to reason about. Form behavior becomes easier to inspect because it is visible directly in the relationships between values.
This approach does not eliminate the hard problems associated with forms. Asynchronous validation, persistence, and interoperability with existing Angular Forms APIs still require careful design. What changes is where that complexity lives. Instead of being interwoven with state representation, those concerns are layered explicitly on top of a clear foundation.
Signal-first forms are not a universal replacement for existing patterns, nor are they a shortcut to simpler applications. They are, however, a strong example of how aligning APIs with first principles can reduce cognitive overhead and improve maintainability over time. For teams building large, state-heavy forms, this alignment can make the difference between code that merely works and code that continues to evolve without friction.
Understanding a reactivity model in the abstract is useful, but it is ultimately incomplete without seeing how it shapes real application code. Concepts such as state, derivation, and explicit dependencies only become meaningful when they influence how forms are built, validated, and maintained in practice.
This article focuses on a concrete example: a modest but realistic registration form. Rather than introducing new concepts, the goal here is to make earlier ideas tangible. We will see how a signal-backed model reshapes validation, interaction state, and submission logic, and how much coordination logic simply disappears when form behavior is expressed declaratively.
The focus here is not on novelty or completeness, but on making the underlying ideas easier to reason about. By walking through a signal-first form from model definition to submission, we can evaluate whether this approach truly reduces complexity and where it introduces new trade-offs that teams should understand before adopting it more broadly.
Implementing a signal-first registration form
With the conceptual groundwork in place, we can now turn theory into a concrete implementation. In this section, we will build a fully working registration form using Angular’s Signal Forms API. This example is deliberately modest in scope, but it is designed to serve as the foundation for the rest of the series. Each subsequent article will extend this same example rather than introducing a new one.
The form collects an email address, a password, a confirmation password, and explicit acceptance of terms. While simple on the surface, this structure allows us to explore field-level validation, cross-field constraints, interaction state, and submission behavior, all without reverting to event-driven form logic.
Project setup and structure
The example assumes a standard Angular application created with the Angular CLI and configured to use Signals (Angular 17+). The Signal Forms APIs (Angular 21+) live under @angular/forms/signals, which must be explicitly imported.
Separating the model from the component keeps form state independent of presentation. This becomes increasingly valuable as the form grows or is reused across multiple components.
Defining the form model
We begin by defining the shape of the data that the form collects. This is a plain TypeScript interface with no Angular dependencies. Treating the form model as a simple data structure reinforces the idea that the form’s values are just state.
This interface mirrors what would typically be sent to a back-end API. There is no duplication of state, no separate “form value” object, and no mapping required at submission time.
Creating the signal-backed form
The form itself is created in the component using a writable signal as the source of truth. The form() function attaches form semantics validation, field state, and submission to that signal.
// registration.component.ts
import { CommonModule } from "@angular/common";
import { Component, signal } from "@angular/core";
import {
email,
form,
FormField,
required,
submit,
} from "@angular/forms/signals";
import { RegistrationData } from "./registration.model";
@Component({
selector: "app-registration",
imports: [FormField, CommonModule],
templateUrl: "./registration.html",
styleUrl: "./registration.css",
})
export class Registration {
readonly model = signal({
email: "",
password: "",
confirmPassword: "",
acceptedTerms: false,
});
readonly registrationForm = form(this.model, (schema) => {
required(schema.email, { message: "Email is required" });
email(schema.email, { message: "Enter a valid email address" });
required(schema.password, { message: "Password is required" });
required(schema.confirmPassword, {
message: "Please confirm your password",
});
required(schema.acceptedTerms, {
message: "You must accept the terms to continue",
});
});
async onSubmit(event?: Event) {
event?.preventDefault();
await submit(this.registrationForm, (value) => {
console.log(value());
// Mock Server Call
return Promise.resolve([
{
kind: "EmailAlreadyExists",
field: this.registrationForm.email,
error: { kind: "server", message: "Email already taken" },
},
]);
});
}
}
Several design decisions are worth noting.
First, the model signal is defined as read-only. All mutations to the model occur through form bindings, not ad hoc assignments in the component. This keeps the component declarative and avoids the temptation to manipulate form state imperatively.
Second, validation is declared in one place. The schema function describes constraints on the model without introducing control trees, validator arrays, or observable pipelines. Angular takes responsibility for re-running validation whenever the model changes.
Finally, submission logic is explicit. The submit() helper ensures that the form is valid before invoking the callback, and it passes the current model value directly. There is no need to check flags or manually extract values.
Binding the form to the template
With the form defined, the next step is to bind it to the template. Signal Forms provide the [formField] directive, which connects an input element directly to a field in the form schema.
@if (registrationForm().errors().length > 0) {
@for (error of registrationForm().errors(); track error.message) {
What stands out here is the absence of indirection. Each input binds directly to a field. Validation state is accessed through signals such as invalid() and touched(). Error messages are read from a structured error object, not reconstructed manually.
This template contains no subscriptions, no async pipes, and no event handlers for value changes. The UI simply reflects the current form state.
Interaction state and user experience
One of the common criticisms of declarative form models is that they obscure user interaction logic. Signal Forms address this directly by exposing interaction metadata as signals.
The touched() signal determines whether a field has been interacted with. By combining it with invalid(), we control when validation messages appear. This logic remains purely declarative: the template describes when errors should be visible, and Angular ensures the signals stay up-to-date.
The disabled state of the submit button is derived from registrationForm.invalid(). There is no need to manually enable or disable it in response to events. If the form becomes valid, the button is enabled automatically.
Why this scales
Even at this early stage, several advantages of a signal-first form model are apparent. The form’s behavior is expressed in terms of state and derivation, not events. The model, validation rules, and UI bindings are clearly separated. There is no duplication of logic between the component and the template.
As the form grows, this structure holds. Additional fields introduce additional schema entries and template bindings, not new subscription logic. Cross-field validation can be added declaratively. Asynchronous validation and persistence can be layered on without rewriting the core model.
Most importantly, the form remains inspectable. At any point during execution, the model signal reflects the current state of the form. Derived state validity, errors, and UI flags can be understood by reading the code, not by tracing runtime behavior.
What we did not solve yet (and why)
At this stage, it would be easy to walk away with the impression that Signal Forms eliminates most of the hard problems associated with form handling. That impression would be misleading. What we have built so far is intentionally incomplete, not because the approach falls short, but because introducing too much too early obscures the value of the underlying model.
One area we have deliberately postponed is cross-field validation that expresses richer business rules. Many real-world forms depend on relationships between fields rather than isolated constraints. Password confirmation is a familiar example, but more complex scenarios quickly arise in enterprise applications. While Signal Forms support these patterns, introducing them before establishing a clear understanding of derived state risks turns validation back into an imperative exercise rather than a declarative one.
We have also avoided asynchronous validation. Server-backed checks introduce latency, partial failure, cancellation, and race conditions. These are not trivial concerns, and treating them casually often leads to subtle bugs and confusing user experiences. Although Signal Forms provide the necessary hooks to model asynchronous behavior, doing so responsibly requires a careful discussion of pending state, effects, and life-cycle boundaries. That discussion belongs in its own article.
Another omission is persistence and synchronization. Many forms need to autosave drafts, synchronize state with local storage, or react to changes by triggering external side effects. These behaviors are not part of the form state itself; they are consequences of state changes. Treating them as such is essential to keeping the architecture comprehensible. Introducing persistence too early would blur the distinction between state and reaction that this article has worked to establish.
Finally, this article has not addressed migration and interoperability. Few teams are starting from a blank slate. Most will adopt Signal Forms incrementally within applications that already rely on reactive forms or template-driven forms. Hybrid approaches, bridging strategies, and gradual refactors are all critical topics, but they presuppose familiarity with both paradigms. Addressing migration before establishing a solid signal-first mental model would undermine that foundation.
These omissions are intentional. A form architecture that tries to do everything at once often ends up doing nothing clearly. By focusing on the core ideas of state, derivation, and declarative validation, we create a base that can absorb additional complexity without collapsing under it.
Signal Forms in the context of Angular’s evolution
To fully appreciate Signal Forms, it helps to step back and view them not as an isolated feature, but as part of a broader shift in Angular’s design philosophy.
For much of its history, Angular emphasized declarative templates paired with imperative coordination in component classes. RxJS became the backbone of that coordination, providing a powerful abstraction for handling asynchronous workflows, user input, and external events. This model scaled well, but it also encouraged developers to express state indirectly through streams and subscriptions.
Signals represent a deliberate recalibration. They re-center Angular’s reactivity model around state and derivation, rather than events and emissions. This shift is visible across the framework: in component inputs, change detection, and now forms. Signal Forms are not an attempt to replace everything that came before; they are an attempt to make the most common use case, modeling and deriving state, simpler and more explicit.
Framed this way, the design of Signal Forms aligns more closely with state-driven form behavior. The requirement to start with a model signal reflects the idea that the state should have a single, inspectable source of truth. Schema-based validation aligns with the notion that constraints are properties of state, not behaviors triggered by events. Field state exposed as signals reinforces the idea that validity, errors, and interaction metadata are derived values that should be read, not managed.
It is also worth noting that Signal Forms do not attempt to abstract away form behavior. They do not hide form state behind opaque classes or life-cycle hooks. They do not require developers to think in terms of control hierarchies or subscription graphs. Instead, they expose form behavior directly, making it easier to reason about how values, validation, and UI feedback relate to one another.
This approach aligns closely with other recent changes in Angular, including the introduction of modern template control flow and a stronger emphasis on explicit data dependencies. Together, these features point toward a framework that favors clarity over indirection and composition over orchestration.
Importantly, Signal Forms are still evolving. Their APIs may change, and their surface area will almost certainly expand. That is precisely why grounding them in first principles matters. Developers who understand why Signal Forms work the way they do will be far better equipped to adapt as the APIs mature.
This article has intentionally avoided duplicating documentation or enumerating every available feature. Instead, it has focused on establishing a conceptual framework that makes the official APIs feel intuitive rather than surprising. When viewed this way, Signal Forms are not a new way to write forms; they are a clearer expression of what forms have always been.
A new way to think about forms
Building the registration form in this article reveals a quiet but important shift. The reduction in complexity does not come from fewer features or simpler requirements. It comes from expressing form behavior in terms of state and derivation rather than orchestration and reaction.
By treating the data model as the single source of truth, validation rules as declarative constraints, and UI behavior as derived from current conditions, much of the coordination logic that typically surrounds forms becomes unnecessary. There are fewer subscriptions to manage, fewer flags to synchronize, and fewer life-cycle concerns to reason about. Form behavior becomes easier to inspect because it is visible directly in the relationships between values.
This approach does not eliminate the hard problems associated with forms. Asynchronous validation, persistence, and interoperability with existing Angular Forms APIs still require careful design. What changes is where that complexity lives. Instead of being interwoven with state representation, those concerns are layered explicitly on top of a clear foundation.
Signal-first forms are not a universal replacement for existing patterns, nor are they a shortcut to simpler applications. They are, however, a strong example of how aligning APIs with first principles can reduce cognitive overhead and improve maintainability over time. For teams building large, state-heavy forms, this alignment can make the difference between code that merely works and code that continues to evolve without friction.
Most generative AI tools know less about R than languages like JavaScript and Python, thanks to how much training data is available for each. However, with a little extra setup, you can give a large language model (LLM) the knowledge it needs to improve its R skills.
Here are 10 ways to help generative AI write R code like a pro.
Use a coding agent
AI coding agents have more power, flexibility, and coding-focused tools than general-purpose chatbots.
Anthropic’s Claude Code and OpenAI’s Codex agents have versions that run in a terminal, IDE extensions, desktop and mobile apps, and other integrations.
R users may also be interested in Posit’s Posit Assistant, which is designed for data analysis in both R and Python. It needs less setup for R than general-purpose coding agents, and it has more built-in knowledge about data science, R package development, and Shiny apps. Plus, it can read objects in your R and Python environments by default, which can be useful in some situations (although perhaps not if you’re working with sensitive data). There are versions of Posit Assistant for RStudio, the Positron IDE, and the terminal. Setup info is here.
Note: Posit Assistant is not the same as the older Positron Assistant that was demo’d at last year’s posit::conf(). The older assistant will eventually be superceded by Posit Assistant. Posit Assistant comes pre-installed with RStudio but not pre-activated, so you don’t need to use it unless you want to.
Google’s original entry in this space, Gemini CLI, is being retired in favor of a new “Antigravity CLI” starting June 18.
There are other IDE platforms with built-in agents, including Cursor and Windsurf, and terminal + subscription coding agents such as Warp, which was open-sourced in April. However, I won’t be covering those here.
You may be limited at work by what your employer allows you to use, but you can still experiment on your own with public data — and then lobby for a new agent if a different one better meets your needs. Whatever you choose, though, moving to one of these from a web-based chatbot should produce better code.
“The shift from chatbot to agent is the most important change in how people use AI since ChatGPT launched,” says Ethan Mollick, co-director of the Wharton School’s Generative AI Labs at the University of Pennsylvania.
Set up CLAUDE.md, AGENTS.md, or GEMINI.md knowledge files
All the major coding agents look for knowledge files to load each time you start a session, and you can easily edit those files. The files can include things like your proficiency in various languages and how you like to document projects. For example, I tell my LLMs that I’d like a README.md in every project with info on how to use the project and technical info on how the project was coded.
Most coding tools let you have both an overarching main file that’s loaded into every session, and an additional file per project.
If you’re adding Claude or Codex to an existing code base, the /init slash command will create one of those .md project files after scanning and analyzing the code. You can also ask an LLM to interview you to create or add to one of those files.
Claude Code looks for a user-level main CLAUDE.md file at ~/.claude/CLAUDE.md and then a project-level file at ./CLAUDE.md or ./.claude/CLAUDE.md. The /memory slash command shows where those files are located. Codex uses AGENTS.md files. Antigravity docs say it looks for a main GEMINI.md file plus GEMINI.md and/or AGENTS.md files in your working directory.
Positron Assistant is set up to read AGENT.md, AGENTS.md, POSITRON.md, CLAUDE.md, GEMINI.md, and LLMS.txt in a workspace root directory, according to the docs. See the Positron docs for more on custom instructions.
Use agent skills
Agent skills can turn an LLM with questionable R knowledge into an expert that understand how you want it to write code. You can develop your own skills, work with your LLM to create skills, or download skills written by others. Claude even has a skill to help you build skills. This is one of the best ways to stop having to repeat the same instructions over and over. Instead, the LLM will “remember” what’s in your skills files by loading them when they’re needed.
What’s the difference between the knowledge .md files mentioned above and a skill? I like to think of CLAUDE.md or AGENTS.md files as important background info that’s always available, while skills are more specific instructions that are useful at certain times. Or, as Claude Opus 4.7 put it: “Skills are for triggered workflows (‘when user asks X, do Y’). A standing rule that applies to every project isn’t trigger-based; it’s a directive I should always follow.” Standing rules should go in CLAUDE.md.
“Create a skill when you keep pasting the same instructions, checklist, or multi-step procedure into chat, or when a section of CLAUDE.md has grown into a procedure rather than a fact,” according to Anthropic’s Claude Code docs. “Unlike CLAUDE.md content, a skill’s body loads only when it’s used, so long reference material costs almost nothing until you need it.” Only skill descriptions are loaded at the start of each session, so the LLM knows what skills are available.
Anthropic originally created skills for Claude in October 2025 and then published the concept as an open standard a couple of months later. Most coding agents now use them.
Anthropic has an easy-to-folllow guide to creating custom skills here if you want to try your hand at building your own.
Posit Assistant comes with several R-related skills built in and allows users to add more. You’ll find them here.
If you’re not using Posit Assistant, you can add Posit-developed skills to another coding agent. Posit published a GitHub repository with skills for creating R packages, Shiny apps, and Quarto documents; for creating and resolving GitHub pull requests; for open-source R and Python package releases; for code reviews (R, Python, JavaScript/TypeScript, SQL); and for creating architectural documentation for a codebase. Browse them all here.
Read and edit the skills you download
Skills are just a folder with one or more markdown text files that have a structured YAML header. That folder can include optional resources and scripts, too. Skill instruction files are easy to read and edit, so you can make sure they do what you want. Tweak them as desired.
For example, Claude helped me write my own customized R skill. Claude leans toward tidyverse packages, but there are other packages I like a lot, too, such data.table. I made sure the R skill knows my package preferences, and under what circumstances I prefer each.
If one of your preferred procedures changes, remember to update the associated skills. Or, ask an LLM to update it for you. Keeping skills up to date will make your coding agent more useful.
Keep your CLAUDE.md or AGENTS.md files updated, too. Maybe you started off with a CLAUDE.md file that said you’re an experienced Python programmer who is just learning R. Three months from now, you might want to update your R skill level. Or, perhaps you told an AGENTS.md file that you like thoroughly documented code, but now you’d prefer a lighter touch. You may want to have your coding agent go over those files with you from time to time and ask you about the main points and whether they need updating.
Use the btw R package and its MCP server
It’s frustrating when an LLM writes code based on an ancient R package version in its training data, or when it doesn’t know about a relatively recent package at all. btw solves this problem by letting your coding agent access info about all the R packages installed on your system. That means it can write code based on your specific R environment. Plus, btw lets an LLM access variables in your current R environment via a Model Context Protocol (MCP) server.
MCP servers are a standardized way for LLMs to access external data — in this case, your running R session (external doesn’t have to mean cloud). Like skills, MCP was created by Anthropic as an open standard and has since been adopted by most major AI platforms. The btw R package includes an MCP server, which you can install for Claude Code by running the following code in a terminal window (not R console).
claude mcp add -s "user" r-btw -- Rscript -e "btw::btw_mcp_server()"
btw comes with a lot of tools. It’s not a great idea to overload your LLM with tools it may never use. If you just want to register btw tools for accessing your R session and looking up package documentation, you can run a command like this instead:
To connect Claude Code to your open R session, you also need to run btw::btw_mcp_session()in every new R session. (I sometimes forget this part until I’m puzzled why Claude can’t read my R variables or even see what packages I have installed.)
You can see a demo of btw in this Posit video that compares Claude Code and Posit Assistant.
The btw R package lets Claude Code and other agents read objects in your R session.
Foundry
You don’t need to do this setup for Posit Assistant, since tools for accessing your R session and variables are built in.
Use plan mode for your projects
I cannot stress this enough. Just as humans benefit from having a plan before starting to implement a project, LLMs benefit from working through a plan before beginning to generate code. Plus, it can be useful at times to “brainstorm” with an LLM about structures and features, ask it for alternatives, or even have another LLM review a plan before implementing it.
Anthropic advises starting with plan mode. “Letting Claude jump straight to coding can produce code that solves the wrong problem. Use plan mode to separate exploration from execution,” they wrote in a best practices doc.
Claude, Codex, Gemini, and Posit Assistant all have a plan mode in their CLI versions that can be activated with the /plan slash command.
Foundry
When in plan mode activated with /plan, Claude will often ask questions before coming up with its plan.
Foundry
Make sure your coding agent learns from its mistakes
This is one of the best tips I read first from Joe Amditis, associate director of operations at the Center for Cooperative Media at Montclair State University: If your coding agent makes a mistake and you work together to fix it, make sure to save that memory so it doesn’t make the same error again.
Your agent may have an auto memory system that’s supposed to do some of this (I’ve used Claude Code’s), but you can instruct any coding agent to keep a separate lessons learned file for each project, too, or ask it what’s the best way to ensure it doesn’t make the same error again. This isn’t a guarantee, any more than you can be assured an LLM will follow all the rules in a CLAUDE.md or AGENTS.md file or all the steps in a skill. But it has definitely cut down on my agents making the same errors over and over.
Have your agent write tests and do code reviews
Tests and code reviews done by a coding agent aren’t a substitute for a human checking that software works. Review an LLM’s results yourself any time a task is important! However, agent-generated tests are still a good way to head off some problems and cut down the time you spend as your LLM’s debugging partner. If you don’t know where to start to help your agent know how to test and review code, see Posit’s testing-r-packages and critical-code-reviewer skills. Your agent may also have built-in code review skills, such as Claude Code’s /code-review slash command.
Code review skills are available from many sources. For example, Sentry has a code review skill in its repository. Aimed specifically at its project’s needs, it can be a useful sample for similar skills. And superpowers is a popular set of general-purpose programming skills that’s billed as “a complete software development methodology for your coding agents, built on top of a set of composable skills and some initial instructions that make sure your agent uses them.” I don’t use superpowers, but a lot of other people clearly do given its more than 227,000 GitHub stars and 20,000 forks.
Another tip: Several experts suggest using a different LLM — maybe even from a different provider — to conduct a code review, since each model has its own strengths and weaknesses.
Don’t forget general prompting good habits
Whatever programming language you’re using, being clear will improve your outputs. Keep your requests targeted, and don’t expect the LLM to read your mind.
“Codex handles complex work better when you break it into smaller, focused steps,” OpenAI says in its developer docs. “Smaller tasks are easier for Codex to test and for you to review. If you’re not sure how to split a task up, ask Codex to propose a plan.”
“Take time to make your prompts as relevant as possible, just as you would when helping a new teammate scope a task,” Google advises in its Five Best Practices for Using AI Coding Assistants. “Consider what details you need to share for a person to succeed, and provide all those details to your AI tool.”
And, don’t load up your context window to the LLM’s published limit. Performance often degrades as you get close to the maximum token limit.
Take advantage of open LLMs, particularly if you have budget and token limits
Open-weight models — especially ones small enough to run on a desktop computer — may never rival frontier LLMs like Claude, GPT, or Gemini. But for a lot of R coding, they don’t need to.
To see how well an LLM can write R code, it should have the same kind of harness that Claude Code, Codex, or Posit Assistant gives larger LLMs. “The harness is the product,” argues Mallory Mejias at Sidecar, an AI education company. “The model is the engine inside it — important, but increasingly interchangeable.” I wouldn’t go that far, since I still find the LLM to be a critical piece of this equation, and models differ. But the same LLM will perform differently depending on the tools and context it has.
Posit recently added Google’s open-source Gemma 4 26B to Assistant’s existing options of larger, closed-source LLMs. “Up until this point, models of this size — small enough to run comfortably on high-end consumer laptops — were on our radar but not yet capable enough to drive an agent harness like Posit Assistant,” Posit senior software engineer Simon Couch wrote on the Posit blog. “This has changed in the last few months with releases like Gemma 4.” However, to use it in Posit Assistant in RStudio, you still need to run it through a Posit AI subscription, not locally. It uses 1/10 the budget that the same session with Claude Sonnet would consume.
Several projects can use Claude Code to run local LLMs. Ollama, open-source software for running LLMs locally, does this with the terminal command ollama launch claude --model , such as:
ollama launch claude --model gemma4:26b
The makers of Unsloth, an open-source framework for running and training models, say running local LLMs inside Claude Code can be very slow due to a cache invalidation issue. They say this can be fixed by adding "CLAUDE_CODE_ATTRIBUTION_HEADER" : "0" to ~/.claude/settings.json under “env”.
Unsloth can also use Claude Code to run local models, but it’s somewhat more involved to install and set up than Ollama. See this Unsloth tutorial.
Open-source coding agent Open Code runs LLMs from OpenAI, Anthropic, Google, Kimi, Alibaba’s Qwen, and others. I haven’t warmed up to it yet compared with commercially backed CLI tools I’ve tried, but it has 174,000 GitHub stars so clearly a lot of fans.
AI consultant Thomas Wiegold, who switched from Claude to Open Code, said “OpenCode covers everything I need for my daily workflow. It’s fast, the provider flexibility is genuinely useful rather than theoretical, and the TUI is better for extended sessions…. That said, I’m not going to pretend it’s all smooth sailing.” He noted some stability issues and a remote code execution vulnerability earlier this year.
Large language models remain imperfect and unpredictable tools, but they’re improving rapidly — as are the harnesses around them. Even frontier LLMs in commercial coding agents can ignore instructions at times and otherwise behave in unpleasantly surprising ways. However, you’ll vastly improve your chances of generating quality code if you use coding agents, take time to set them up with quality instructions, and remember good prompting techniques. Good luck!
—
Coding agent info
Claude Code – Anthropic’s coding agent is available in the terminal and as an IDE extension, a desktop app, mobile apps, Slack app, and a cloud version that can work directly in your repos on GitHub. Arguably the first breakthrough coding agent, it now includes an elegant remote-control option where you can start a session on your desktop and then continue it on a phone or tablet. While designed for Claude LLMs, you can use it to run local LLMs via tools like Ollama or Unsloth Studio. When using Claude models, it tends to offer less usage than other vendors’ options. Anthropic has posted the terminal version’s code on GitHub, with usage governed by Anthropic’s commercial terms of service. You can use Claude Code via API pay per use or as part of a Claude subscription.
Codex – OpenAI’s answer to Claude Code is available via terminal, app, IDE extension, or cloud, with integrations for GitHub, Slack, and Linear. The CLI tool is open source under an Apache license, available on GitHub, and works via API pay per use or as part of a ChatGPT subscription.
Posit Assistant – The Posit (formerly RStudio) coding agent is designed for data work in R and Python, although Posit senior software engineer Simon Couch told me it would do fine for programming work for other uses and in other languages. It is available for the RStudio IDE (via a Posit AI subscription in RStudio), Positron IDE, and as a stand-alone CLI terminal app, although the CLI tool currently doesn’t feel as full-featured as the IDE integrations (that may change). As of this writing, you can also use API keys from Anthropic, OpenAI, and Snowflake Cortex and a GitHub Copilot account in Positron. Posit Assistant is being updated quite frequenty, Couch told me. If you tested it a month or two ago, it has more capabilities now.
Antigravity CLI – Google’s terminal-based tool will be replacing the Gemini CLI for unpaid and Google One users this month. There’s also an Antigravity IDE, SDK, and platform for orchestrating multiple agents. One advantage of Antigravity: Google offers a free plan with access to Gemini 3.5 Flash, Gemini 3.1 Pro, Gemini 3 Flash, Claude Sonnet 4.6, Claude Opus 4.6, and gpt-oss-120b, and what it describes as “generous” but undefined weekly rate limits based on “the degree we have capacity.”
Between 2029 and 2032, every currently supported long-term support (LTS) version of Java will reach end-of-support within a single three-year window: Java 17 in 2029, Java 8 in 2030, Java 21 in 2031, and Java 11 in 2032.
On paper, this looks like a manageable upgrade cycle. In practice, it creates a collision of timelines that most enterprises have failed to forecast. Organizations attempting to modernize incrementally—moving application by application, version by version—are operating on a model that the calendar has already rendered obsolete.
The primary danger here is the illusion of time. Traditional modernization plans rely on sequential upgrades and controlled pacing. However, when every major Java version expires in the same compressed window, sequential planning collapses. By the time this becomes obvious, organizations will be forced into reactive mode, making rushed decisions under extreme pressure.
The modernization illusion
For organizations planning traditional stepwise upgrades—Java 8 to Java 11 to Java 17 to Java 21—this convergence elevates a routine maintenance task into a structural crisis. Enterprises with large Java estates will be forced to upgrade multiple applications across multiple versions simultaneously to maintain security compliance and business continuity. Waiting until the late 2020s to act guarantees a modernization process under emergency conditions.
While modern Java versions maintain strong backward compatibility, they cannot offset the drag of what enterprises are carrying forward: decades of accumulated technical debt.
In large Java environments, technical debt is pervasive. It exists as unused libraries, obsolete logic, forgotten dependencies, and dormant features—quietly inflating the size, risk, and complexity of every modernization effort. In many organizations, a significant portion of the codebase no longer executes in production, yet it still consumes developer attention, security oversight, and planning effort.
As codebases grow older and larger, this drag compounds. What looks like a simple version upgrade on a roadmap becomes a massive operational burden in practice.
Why incremental planning fails
Most modernization strategies assume that upgrades can be sequenced and absorbed gradually. That assumption is now dangerous. When multiple Java versions reach end-of-support in the same narrow window, enterprises don’t face a single modernization project—they face parallel modernization across their entire estate.
This shifts the challenge from engineering complexity to organizational capacity.
Consider a typical enterprise with 100 developers. If even a fraction of their time is spent maintaining, investigating, or working around unused and obsolete code, the organization burns meaningful engineering capacity on work that delivers no business value. Multiply that across dozens or hundreds of applications, and the bottleneck becomes clear: modernization is limited by people, not frameworks.
Parallel modernization requires parallel capacity—something most organizations haven’t budgeted for.
This explains why traditional approaches struggle to scale. Tools that analyze code in isolation cannot distinguish what actually matters in production. Without clear visibility into what code is relevant, organizations default to caution, effectively converting their timelines into risk.
The real bottleneck: developer capacity
The Java modernization crunch is a crisis of resource allocation, not a technology problem.
Every hour developers spend maintaining obsolete code or investigating unused dependencies is an hour lost to modernization. When organizations face simultaneous upgrades across multiple applications, human capacity becomes the limiting factor. Sequential planning and parallel modernization require the time and capacity most enterprises no longer have.
Organizations that delay action are consuming their flexibility rather than preserving it. Each year of inaction increases the volume of code that must be moved, reviewed, secured, and modernized within the same fixed window. By the time deadlines become unavoidable, the only remaining options are compression, shortcuts, and uncomfortable trade-offs.
A different way to think about readiness
The organizations that navigate this transition successfully will prioritize clarity over immediate upgrades.
Modernization at scale requires an accurate understanding of what actually matters in production before attempting to move it forward. Without that visibility, every upgrade effort inherits unnecessary complexity, consumes excess capacity, and introduces avoidable risk.
The goal is not simply adopting better tools, but reducing the structural load enterprises carry into modernization. Leaner systems modernize faster. Simpler estates scale better. Complexity compounds under time pressure.
The timeline is already set
The Java modernization crunch is a timing problem that is already locked in.
Enterprises that treat the next few years as business-as-usual will discover that sequential plans cannot survive compressed timelines. Those that confront technical debt now—before the pressure hits—will find the coming transition difficult but manageable. Those that don’t will face rushed decisions and permanent trade-offs.
By the time 2029 arrives, the window for gradual modernization will have closed. The calendar won’t wait for us to be ready.
—
New Tech Forum provides a venue for technology leaders—including vendors and other outside contributors—to explore and discuss emerging enterprise technology in unprecedented depth and breadth. The selection is subjective, based on our pick of the technologies we believe to be important and of greatest interest to InfoWorld readers. InfoWorld does not accept marketing collateral for publication and reserves the right to edit all contributed content. Send all inquiries to doug_dineley@foundryco.com.