Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
150720 stories
·
33 followers

Introducing the TeamCity Operator for Kubernetes

1 Share

Running TeamCity in Kubernetes has always been possible, but it often required knowing your way around both systems pretty well.

TeamCity is flexible and easy to tune, and Kubernetes provides excellent tools for running applications at scale. However, bringing the two together smoothly wasn’t always straightforward.

The new TeamCity Operator is designed to change that. It uses custom resources to automate the management of your TeamCity server life cycle. For example, it allows you to automatically deploy new servers and perform zero-downtime upgrades (see the Kubernetes documentation for more details).

Although the TeamCity Operator was originally designed to support internal use cases, interest from users running sizable Kubernetes-based installations encouraged us to make it open source. It’s now publicly available and ready for you to explore.

Why an operator?

The most common way to deploy applications to Kubernetes is through Helm charts. Helm works well for many services, but it has limitations when it comes to applications that require orchestrated, stateful operations, such as controlled upgrades, multi-node setups, or zero-downtime updates.

TeamCity fits squarely into this category. Its life cycle is complex and nuanced. Updates require sequencing, certain states must be preserved, and there are moments when two versions must temporarily coexist.

TeamCity OperatorTeamCity Helm charts
Go applicationCollection of templated YAML files
Single object that controls everything needed for TeamCityNo control over the life cycle
Minimal need to think about dependencies in KubernetesUpdateable fields in objects only

Kubernetes operators exist precisely for these cases. An operator is an application that runs inside a cluster and encodes expert knowledge about how to deploy, upgrade, and maintain a particular system. Instead of asking you to understand every nuance of TeamCity’s Kubernetes behavior, the TeamCity Operator takes on that work.

How it works

The TeamCity Operator introduces a new Kubernetes custom resource that represents a TeamCity installation. When you apply it to the cluster, the TeamCity Operator inspects this object and turns it into a fully configured, running TeamCity instance.

In practice, this means the TeamCity Operator:

  • Creates and configures the necessary StatefulSets.
  • Ensures the startup and shutdown sequences are executed correctly.
  • Applies changes to the TeamCity installation based on updates to the custom resource specification.
  • Manages upgrades, including optional zero-downtime ones.
  • Coordinates the main and secondary nodes in multi-node setups.

The TeamCity Operator continuously watches the Kubernetes API for changes. When you update the TeamCity custom resource spec (for example, by changing the version or resource settings), the TeamCity Operator reconciles the actual state with the desired one.

In other words, you declare what TeamCity should look like, and the TeamCity Operator makes your vision a reality.

Example: Deploying a TeamCity main node with an external database

To give you a concrete idea of how the TeamCity Operator is used in practice, here is an example of deploying a standalone TeamCity main node that connects to an external database.

This example demonstrates how database configuration is passed via a Kubernetes Secret and how the TeamCity custom resource defines the main node, resources, and storage.

apiVersion: v1
data:
  connectionProperties.password: DB_PASSWORD
  connectionProperties.user: DB_USER
  connectionUrl: DB_CONNECTION_STRING # format jdbc:mysql://DB_HOST:DB_PORT/SCHEMA_NAME
kind: Secret
metadata:
  name: database-properties
---
apiVersion: jetbrains.com/v1beta1
kind: TeamCity
metadata:
  name: teamcity-with-database
  namespace: default
  finalizers:
    - "teamcity.jetbrains.com/finalizer"
spec:
  image: jetbrains/teamcity-server
  databaseSecret:
    secret: database-properties
  mainNode:
    name: main-node
    spec:
      env:
        AWS_DEFAULT_REGION: "eu-west-1"
      requests:
        cpu: "900m"
        memory: "1512Mi"
  dataDirVolumeClaim:
    name: teamcity-data-dir
    volumeMount:
      name: teamcity-data-dir
      mountPath: /storage
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi

Built-in life cycle management

We’ve spent years refining the life cycle of a TeamCity server (startup, updates, and clean shutdown) for TeamCity Cloud. The new operator brings these practices to self-hosted users.

For upgrades, the TeamCity Operator can perform a quick update with downtime or run an orchestrated, zero-downtime upgrade process, keeping at least one TeamCity node available while switching versions behind the scenes

These workflows go far beyond what a Helm chart can reliably encode.

Configuration as code with the TeamCity Operator, Terraform, and the Kotlin DSL

The TeamCity Operator is focused on life cycle and cluster-level configuration. It gives you a running TeamCity instance but intentionally leaves higher-level configuration to other tools.

You can combine it with:

  • The Terraform Provider for TeamCity for managing server-level settings, cleanup rules, VCS roots, project templates, and other global configuration elements.
  • The Kotlin DSL or YAML for per-project build configurations.

Together, these create a complete and consistent code-driven setup:

  • The TeamCity Operator installs, scales, upgrades, and tears down TeamCity.
  • Terraform configures global server behavior.
  • The Kotlin DSL or YAML defines the builds themselves.

This allows you to manage every part of TeamCity with code and keep the configuration fully version-controlled.

Why use the TeamCity Operator?

To put it simply, the TeamCity Operator offers a hassle-free way to run TeamCity in Kubernetes.

It gives you:

  • A minimal starting manifest for quick setups.
  • An extended manifest for more advanced installations.
  • An automated life cycle that reduces the risk of misconfiguration.
  • Consistent behavior across environments.
  • A path toward a fully code-driven, reproducible CI/CD infrastructure.

If you’re running TeamCity as part of your CI/CD setup (especially in a Kubernetes-heavy environment), the TeamCity Operator helps take some of the pressure off. It reduces your day-to-day workload and makes long-term maintenance much easier.

What’s next

We’re preparing official documentation to accompany the TeamCity Operator. In the meantime, you can explore the GitHub repository, which includes solid documentation in the README.

For now, the GitHub repository includes examples and technical notes, and we’ll expand them over time. We also plan to publish migration guides and usage examples, particularly around combining the TeamCity Operator with Terraform and the Kotlin DSL.

You’ll also see the TeamCity Operator featured at industry events such as KubeCon, where our engineers will be available to give a walk-through of the design and answer questions.

👉 Check out the GitHub repository 👈

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

OpenAI report reveals a 6x productivity gap between AI power users and everyone else

1 Share

The tools are available to everyone. The subscription is company-wide. The training sessions have been held. And yet, in offices from Wall Street to Silicon Valley, a stark divide is opening between workers who have woven artificial intelligence into the fabric of their daily work and colleagues who have barely touched it.

The gap is not small. According to a new report from OpenAI analyzing usage patterns across its more than one million business customers, workers at the 95th percentile of AI adoption are sending six times as many messages to ChatGPT as the median employee at the same companies. For specific tasks, the divide is even more dramatic: frontier workers send 17 times as many coding-related messages as their typical peers, and among data analysts, the heaviest users engage the data analysis tool 16 times more frequently than the median.

This is not a story about access. It is a story about a new form of workplace stratification emerging in real time — one that may be reshaping who gets ahead, who falls behind, and what it means to be a skilled worker in the age of artificial intelligence.

Everyone has the same tools, but not everyone is using them

Perhaps the most striking finding in the OpenAI report is how little access explains. ChatGPT Enterprise is now deployed across more than 7 million workplace seats globally, a nine-fold increase from a year ago. The tools are the same for everyone. The capabilities are identical. And yet usage varies by orders of magnitude.

Among monthly active users — people who have logged in at least once in the past 30 days — 19 percent have never tried the data analysis feature. Fourteen percent have never used reasoning capabilities. Twelve percent have never used search. These are not obscure features buried in submenus; they are core functionality that OpenAI highlights as transformative for knowledge work.

The pattern inverts among daily users. Only 3 percent of people who use ChatGPT every day have never tried data analysis; just 1 percent have skipped reasoning or search. The implication is clear: the divide is not between those who have access and those who don't, but between those who have made AI a daily habit and those for whom it remains an occasional novelty.

Employees who experiment more are saving dramatically more time

The OpenAI report suggests that AI productivity gains are not evenly distributed across all users but concentrated among those who use the technology most intensively. Workers who engage across approximately seven distinct task types — data analysis, coding, image generation, translation, writing, and others — report saving five times as much time as those who use only four. Employees who save more than 10 hours per week consume eight times more AI credits than those who report no time savings at all.

This creates a compounding dynamic. Workers who experiment broadly discover more uses. More uses lead to greater productivity gains. Greater productivity gains presumably lead to better performance reviews, more interesting assignments, and faster advancement—which in turn provides more opportunity and incentive to deepen AI usage further.

Seventy-five percent of surveyed workers report being able to complete tasks they previously could not perform, including programming support, spreadsheet automation, and technical troubleshooting. For workers who have embraced these capabilities, the boundaries of their roles are expanding. For those who have not, the boundaries may be contracting by comparison.

The corporate AI paradox: $40 billion spent, 95 percent seeing no return

The individual usage gap documented by OpenAI mirrors a broader pattern identified by a separate study from MIT's Project NANDA. Despite $30 billion to $40 billion invested in generative AI initiatives, only 5 percent of organizations are seeing transformative returns. The researchers call this the "GenAI Divide" — a gap separating the few organizations that succeed in transforming processes with adaptive AI systems from the majority that remain stuck in pilots.

The MIT report found limited disruption across industries: only two of nine major sectors—technology and media—show material business transformation from generative AI use. Large firms lead in pilot volume but lag in successful deployment.

The pattern is consistent across both studies. Organizations and individuals are buying the technology. They are launching pilots. They are attending training sessions. But somewhere between adoption and transformation, most are getting stuck.

While official AI projects stall, a shadow economy is thriving

The MIT study reveals a striking disconnect: while only 40 percent of companies have purchased official LLM subscriptions, employees in over 90 percent of companies regularly use personal AI tools for work. Nearly every respondent reported using LLMs in some form as part of their regular workflow.

"This 'shadow AI' often delivers better ROI than formal initiatives and reveals what actually works for bridging the divide," MIT's Project NANDA found.

The shadow economy offers a clue to what's happening at the individual level within organizations. Employees who take initiative — who sign up for personal subscriptions, who experiment on their own time, who figure out how to integrate AI into their workflows without waiting for IT approval — are pulling ahead of colleagues who wait for official guidance that may never come.

These shadow systems, largely unsanctioned, often deliver better performance and faster adoption than corporate tools. Worker sentiment reveals a preference for flexible, responsive tools — precisely the kind of experimentation that separates OpenAI's frontier workers from the median.

The biggest gaps show up in technical work that used to require specialists

The largest relative gaps between frontier and median workers appear in coding, writing, and analysis — precisely the task categories where AI capabilities have advanced most rapidly. Frontier workers are not just doing the same work faster; they appear to be doing different work entirely, expanding into technical domains that were previously inaccessible to them.

Among ChatGPT Enterprise users outside of engineering, IT, and research, coding-related messages have grown 36 percent over the past six months. Someone in marketing or HR who learns to write scripts and automate workflows is becoming a categorically different employee than a peer who has not — even if they hold the same title and started with the same skills.

The academic research on AI and productivity offers a complicated picture. Several studies cited in the OpenAI report find that AI has an "equalizing effect," disproportionately helping lower-performing workers close the gap with their higher-performing peers. But the equalizing effect may apply only within the population of workers who actually use AI regularly. A meaningful share of workers are not in that group at all. They remain light users or non-users, even as their more adventurous colleagues pull away.

Companies are divided too, and the gap is widening by the month

The divide is not only between individual workers. It exists between entire organizations.

Frontier firms — those at the 95th percentile of adoption intensity — generate approximately twice as many AI messages per employee as the median enterprise. For messages routed through custom GPTs, purpose-built tools that automate specific workflows, the gap widens to seven-fold.

These numbers suggest fundamentally different operating models. At median companies, AI may be a productivity tool that individual workers use at their discretion. At frontier firms, AI appears to be embedded in core infrastructure: standardized workflows, persistent custom tools, systematic integration with internal data systems.

The OpenAI report notes that roughly one in four enterprises still has not enabled connectors that give AI access to company data—a basic step that dramatically increases the technology's utility. The MIT study found that companies that purchased AI tools from specialized vendors succeeded 67 percent of the time, while internal builds had only a one-in-three success rate. For many organizations, the AI era has technically arrived but has not yet begun in practice.

The technology is no longer the problem — organizations are

For executives, the data presents an uncomfortable challenge. The technology is no longer the constraint. OpenAI notes that it releases a new feature or capability roughly every three days; the models are advancing faster than most organizations can absorb. The bottleneck has shifted from what AI can do to whether organizations are structured to take advantage of it.

"The dividing line isn't intelligence," the MIT authors write. The problems with enterprise AI have to do with memory, adaptability, and learning capability. Problems stem less from regulations or model performance, and more from tools that fail to learn or adapt.

Leading firms, according to the OpenAI report, consistently invest in executive sponsorship, data readiness, workflow standardization, and deliberate change management. They build cultures where custom AI tools are created, shared, and refined across teams. They track performance and run evaluations. They make AI adoption a strategic priority rather than an individual choice.

The rest are leaving it to chance — hoping that workers will discover the tools on their own, experiment on their own time, and somehow propagate best practices without infrastructure or incentive. The six-fold gap suggests this approach is not working.

The window to catch up is closing faster than most companies realize

With enterprise contracts locking in over the next 18 months, there's a shrinking window for vendors and adopters to cross the divide.The GenAI Divide identified by the MIT report is not going to last forever. But the organizations that figure out a way across it soonest will be the ones that define the next era of business.

Both reports carry caveats. The OpenAI data comes from a company with an obvious interest in promoting AI adoption. The productivity figures are self-reported by customers already paying for the product. The MIT study, while independent, relies on interviews and surveys rather than direct measurement. The long-term effects of this technology on employment, wages, and workplace dynamics remain uncertain.

But the core finding — that access alone does not produce adoption, and that adoption varies enormously even within organizations that have made identical tools available to all — is consistent with how previous technologies have diffused through the economy. Spreadsheets, email, and the internet all created similar divides before eventually becoming universal. The question is how long the current gap persists, who benefits during the transition, and what happens to workers who find themselves on the wrong side of it.

For now, the divide is stark. Ninety percent of users said they prefer humans for "mission-critical work," while AI has "won the war for simple work." The workers who are pulling ahead are not doing so because they have access their colleagues lack. They are pulling ahead because they decided to use what everyone already has—and kept using it until they figured out what it could do.

The 6x gap is not about technology. It is about behavior. And behavior, unlike software, cannot be deployed with a company-wide rollout.



Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

AI-assisted coding for teams that can't get away with vibes

1 Share

Status: Living document based on production experience
Last updated: 5-Jun-2025

AI should be adopted by serious engineering teams that want to build thoughtful, well-crafted products. This requires skillful usage of these tools. Our obsession with building high-quality software for over a decade has driven us to figure out how this new way of building can result in better products.

Building with AI is fast. The gains in velocity are important, because when harnessed correctly, it allows teams to tighten feedback loops with users faster and make better products.

Yet, AI tools are tricky to use. Hold it wrong, and you can generate underwhelming results, worse still, slow down your velocity by drowning your project in slop and technical debt.

This living playbook is based on our experience working with AI tools in the messy trenches of production software, where no one can afford to get away with vibes. I hope other teams can learn and benefit from our findings.

AI is a multiplier

To make AI good, get good yourself. AI is a multiplier. If you are a small coefficient, you won’t see much gain. If you are a negative coefficient, expect negative gains.

What I have noticed is that the best and most experienced engineers are able to extract a lot more out of AI tools. There are several reasons for this:

  • They are extremely good at communicating technical ideas.
  • They have a keen calibration and feel for what leads to a good system and can steer LLMs accordingly, i.e., they have what I like to call “the mechanic’s touch”.
  • They have strong fundamentals, so they immediately get up to speed with new tools and systems where knowledge, not skill is the bottleneck.
  • AI is still sensitive to language and style and will often mirror the tastes and sensibilities of the prompter. Highly skilled engineers have really sharpened taste and instinct for what works and what doesn’t.

Therefore, embody the care of a craftperson. At the end of the day, you should produce artifacts you are proud of, even if the AI assisted in making it. This has translated well into the output I am seeing from these systems.

Here’s an example. This prompt is not unreasonable but not particularly thoughtful:

Write a Python rate limiter that limits users to 10 requests per minute.

I would expect this prompt to give okay results, but also miss some edge cases, good practices and quality standards. This is how you might see someone at nilenso prompt an AI for the same task:

Implement a token bucket rate limiter in Python with the following requirements:

- 10 requests per minute per user (identified by `user_id` string)
- Thread-safe for concurrent access
- Automatic cleanup of expired entries
- Return tuple of (allowed: bool, retry_after_seconds: int)

Consider:
- Should tokens refill gradually or all at once?
- What happens when the system clock changes?
- How to prevent memory leaks from inactive users?

Prefer simple, readable implementation over premature optimization. Use stdlib only (no Redis/external deps).

Guess which prompt is going to better achieve the program designer’s intent?

A technique that has worked well for us is metaprompting. I prompt the model with a simple task and ask it to help surface tradeoffs and edge cases. Then I turn it into a tech spec and hand it off to another LLM agent to execute. Even the “better prompt” I shared above is a result of asking the AI to come up with a good prompt. From my experience, models have become good at prompting themselves.

The mechanics of what works for these tools are in flux, but one robust principle is to really work on yourself to be a good engineer. Your habits will quickly pass on to the AI systems you work with. The reason this works is because what helps the human helps the AI.

What helps the human helps the AI

I shall clarify what software engineering is, something I found worth revisiting in the light of disruption from AI advancements.

Software engineering is not about writing code. Or at least, that’s not the defining characteristic, much like how writing is not wrist exercises with ink on paper.

To me software engineering is the art and science of maintaining a large body of well-defined mental models that achieve a business or economic need. Much of the work is around crafting and curating these large, complex sociotechnical systems, and code is just one representation of these systems.

Until AI is good enough to engulf this whole sociotechnical system and expel out all the humans cultivating it, it has to participate and benefit from this very system. In simpler words: AI thrives far, far better in an environment in which a human would also thrive. Which means your team’s software fundamentals should be strong.

A system in which AI thrives is one with markers of a high quality team and codebase. These are:

  • Good test coverage, with useful assertions
  • Automated linting, formatting and test checks before code merges
  • Continuous integration and deployment
  • Well documented changes, tech specs, ADRs with good commit messages
  • Consistent styles and patterns, enforced through a formatter
  • Simple, concise, well-organised code
  • Clearly defined features, broken down into multiple small story cards

Today’s AI can and will make use of all these things to make things “just work”. When I give a coding agent a task, it keeps correcting itself in the agentic loop by running the test cases and static analysis tools. This greatly reduces the hand-holding and intervention needed to get work done.

A rich environment and context helps the AI work better.

Here’s an anecdote: when I was working at a project that had two services, one of them had all of the things I described above—good tests, well-documented changes, consistent patterns in the code, lots of checks and guardrails. The other service was messier and had none of the above. Our AI coding assistant struggled to complete a task of equal difficulty on the latter codebase when compared to the former! This is likely because the messier codebase was as confusing for the AI as it would be for a human. There were mixed signals about the right way to do things.

Now that I have outlined the general strategy, here are some tactics that have helped me.

Use the best frontier AI models, don’t cheap out.

  • Use the best coding model available. Do not try to save credits and cost by using a worse model. The goodness of a good model compounds. All the tactics that I present ahead will work far better when you have a strong coding model to begin with.

Be excellent at providing context.

  • The effectiveness of AI-assisted coding is strongly dependent on how skillfully you can provide the right context to the LLM.
  • Use an “agentic” coding tool. These are tools that are able to read and analyse files, run shell commands, fetch docs, create plans and execute on those plans, needing no human intervention (except maybe approvals). Our current recommendation for tools that do this are Claude Code, Windsurf, Cursor, Cline.
  • LLMs can get distracted and fall into rabbitholes if given irrelevant or a cluttered context. Focus its attention by only @-mentioning files that are relevant and linking only to documentation that helps the task.
  • Encode coding standards and practices in a RULES.md file. Symlink this file to agent specific rules files such as .cursorrules, .windsurfrules, claude.md, agents.md etc
    • This file should have information about the tech stack, how to use the dev tooling and run the linter, coding standard and patterns, and cover for common mistakes that the LLMs have made when working with the code. Here’s an example.

Implementing a new feature or refactor

  • Break down the problem. AI works better the more specific you are. Remember, you can also use the AI to reduce the tedium of making your prompts better written and more specific. Reasoning models are great at this!
  • If you are working on a big feature, break it down into small tasks, and feed the tasks one by one, making a commit at the end of each task. If you do these with your stories, the story card description with the task list is often a very helpful description for the AI.
  • Supply tech specs and relevant documentation about the product and feature. Don’t just ask it to write code without broader context of the product. Also feed it documentation on how to use the libraries you are using. Pasting links to documentation often works with most tools. Some libraries provide a llms.txt for coding agents to use.
  • Another pattern that has worked well for us is to break down the feature into “planning” and “execution” stages. Some coding agents already do this kind of a breakdown for you.
  • Do not take AI suggestions for granted. Ask it to justify its choices, present alternatives and think about advantages and drawbacks.

Debugging

  • Use AI to debug errors in its generation. Always paste the error context most relevant for the LLM to help it understand the issue (I prefer to delineate the error logs or output in a separate XML tag).
  • Explain what you have tried, and additional observations to help the model generate correct hypotheses and eliminate bad ones. Provide lots of context.

Use AI to grow your own skills and knowledge

  • LLMs are an infinitely patient teacher with massive world knowledge (and more recently, ability to research effectively). Aggressively use them to learn things and demystify any new code or stack. Relentlessly dig. Figure out the best practices. Ensure you are learning correctly by getting the LLM to cite high quality sources.

Create extensive documentation

  • Create lots of detailed documentation easily by feeding codebases to the LLM. Egs:
    • Explain functionality, create a knowledge base
    • Summarise all the current metrics being collected
    • Identify missing test cases more intelligently

There’s a good reason to do this—documentation is now cheap to generate and feeds back into making your LLMs (and humans) on the project a lot more effective.

Microfriction lubricants

LLMs greatly reduce the cost of creating lubricants for all the minor friction points that teams run into on a daily basis.

  • Use them to create mockservers to coordinate and unblock work between frontend and backend teams. All that is needed is agreeing on a contract.
  • Create runbooks and guides for infra deployments, common types of troubleshooting and more by supplying shell history sessions to the LLM.
  • Feed existing runbooks and guides to an LLM to make them into scripts automating common tasks.

Code review

  • Have a template for Pull Requests, feed the code diff (git log -p <range>) of each feature to the AI to explain the changes and how to deploy them. Some tools can already do this for you.
  • To reduce time to first PR review, use a code reviewing bot for the first part. But do not replace human review!
  • Use LLMs to explain a change that you don’t fully understand as a reviewer. Ask it for clarification, and then ask the implementer after gathering the necessary context.

Debugging and monitoring live applications

  • Use researching capabilities of LLMs to help find solutions to uncommon errors. Follow the advice of debugging in the editor to debug outside it. Provide as much context as you can.
  • LLMs are quite decent at writing queries and alerting rules for observability tools. They also are good at crunching data and performing analyses by writing custom python code.

Performance optimisations

  • Use LLMs to help you optimise databases and tune configuration. When doing so provide context on the infrastructure and hardware. Share query plans.
  • This is an example of such an interaction I had recently.

Implications on how AI changes craft

This is a huge shift in how we write software, and I believe it warrants some changes to ideas that were previously considered common wisdom.

Firstly, It’s less valuable to spend too much time looking for and building sophisticated abstractions. DRY is useful for ensuring patterns in the code don’t go out of sync, but there are costs to implementing and maintaining an abstraction to handle changing requirements. LLMs make some repetition palatable and allow you to wait a bit more and avoid premature abstraction.

Redoing work is now extremely cheap. Code in the small is less important than structural patterns and organisation of the code in the large. You can also build lots of prototypes to test an idea out. For this, vibe-coding is great, as long as the prototype is thrown away and rewritten properly later.

Working with LLMs also lets you take advantage of the generator-verifier gap. Often it’s easier to verify and fix things than it is to produce them from scratch. This reduces activation energy to try new things.

Tests are non-negotiable, and AI removes all excuses to not write them because of how fast they can belt them out. But always review the assertions!

  • Deploying autonomous agents like Devin/Jules/Claude Code and using them well
  • AI tooling for writing queries, performing data analysis
  • Concerns with leaking proprietary code, hosted LLM options, etc
  • Building a culture of sharing prompts, patterns and templates
  • Effective ways of driving AI adoption in teams

References

Programming with LLMs - David Crawshaw

Here’s how I use LLMs to help me write code - Simon Willison

How I use “AI” - Nicholas Carlini

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Agentic Coding Recommendations

1 Share

written on June 12, 2025

There is currently an explosion of people sharing their experiences with agentic coding. After my last two posts on the topic, I received quite a few questions about my own practices. So, here goes nothing.

Preface

For all intents and purposes, here’s what I do: I predominently use Claude Code with the cheaper Max subscription for $100 a month 1. That works well for several reasons:

  • I exclusively use the cheaper Sonnet model. It’s perfectly adequate for my needs, and in fact, I prefer its outputs over the more expensive Opus model.

  • I optimize my tool usage to be token efficient. I avoid screenshots and browser interactions wherever possible. More on that later.

My general workflow involves assigning a job to an agent (which effectively has full permissions) and then waiting for it to complete the task. I rarely interrupt it, unless it’s a small task. Consequently, the role of the IDE — and the role of AI in the IDE — is greatly diminished; I mostly use it for final edits. This approach has even revived my usage of Vim, which lacks AI integration.

One caveat: I expect this blog post to age very poorly. The pace of innovation here is insane; what was true a month ago barely holds true today. That’s why I’m sticking to concepts I believe have staying power.

If you want to a small session of me working on an Open Source library with it, I have a recording you can watch.

The Basics

I disable all permission checks. Which basically means I run claude --dangerously-skip-permissions. More specifically I have an alias called claude-yolo set up. Now you can call that irresponsible and there are definitely risks with it, but you can manage those risks with moving your dev env into docker. I will however say that if you can watch it do its thing a bit, it even works surprisingly well without dockerizing. YMMV.

MCP. This is a term you cannot avoid. It basically is a standardized protocol to give agents access to more tools. Honestly: at this point I barely use it, but I do use it. The reason I barely use it is because Claude Code is very capable of just running regular tools. So MCP for me is really only needed if I need to give Claude access to something that finds too hard to use otherwise. A good example for this is the playwright-mcp for browser automation. I use it because I haven’t found anything better yet. But for instance when I want my agent to poke around in my database, I just uses whatever it finds to be available. In my case it loves to use psql and that’s more than good enough.

In general I really only start using MCP if the alternative is too unreliable. That’s because MCP servers themselves are sometimes not super reliable and they are an extra thing that can go wrong. Trying to keep things very simple. My custom tools are normal scripts that it just runs.

Choice Of Language

I’ve evaluated agent performance across different languages my workload, and if you can choose your language, I strongly recommend Go for new backend projects. Several factors strongly favor Go:

  • Context system: Go provides a capable copy-on-write data bag that explicitly flows through the code execution path, similar to contextvars in Python or .NET’s execution context. Its explicit nature greatly simplifies things for AI agents. If the agent needs to pass stuff to any call site, it knows how to do it.

  • Test caching: Surprisingly crucial for efficient agentic loops. In Rust, agents sometimes fail because they misunderstand cargo test‘s invocation syntax. In Go, tests run straightforwardly and incrementally, significantly enhancing the agentic workflow. It does not need to figure out which tests to run, go does.

  • Go is sloppy: Rob Pike famously described Go as suitable for developers who aren’t equipped to handle a complex language. Substitute “developers” with “agents,” and it perfectly captures why Go’s simplicity benefits agentic coding.

  • Structural interfaces: interfaces in Go are structural. If a type has the methods an interface expects, then it conforms. This is incredibly easy for LLMs to “understand”. There is very little surprise for the agent.

  • Go has low eco-system churn: Go’s entire ecosystem embraces backwards compatiblity and explicit version moves. This greatly reduces the likelihood of AI generating outdated code — starkly contrasting JavaScript’s fast-moving ecosystem for instance.

For comparison, Python — my initial choice — often poses significant challenges. Agents struggle with Python’s magic (eg: Pytest’s fixture injection) or complex runtime challenges (eg: wrong event loop when working with async), frequently producing incorrect code that even the agentic loop has challenges resolving. Python also has practical performance problems. I don’t mean that it writes slow code, i mean that the agent loop is really slow. That’s because the agent loves to spawn processes and test scripts, and it can take quite a while for the interpreter to boot up and initialize the entire application.

On the frontend I settled on tailwind, react with tanstack’s query and router as well as vite. I’m not amazingly happy with it, but I found it better than the alternatives. Tailwind and vite are great, no complaints there. Tanstack’s file based router does not make me super happy. In parts because it likes to have dollar signs in the file names and those really like to confuse the agent. For instance it’s quite common that it tries to edit $param.tsx but it edits the file .tsx instead because it to confused by shell interpolation. It’s a minor thing, but a very annoying thing.

Tools, Tools, Tools

Regardless of language, effective tooling is paramount. Key rules:

  • Anything can be a tool. A shell script can be a tool, an MCP server can be a tool, a log file can be a tool. If the agent can interact or observe it, it counts.

  • Tools need to be fast. The quicker they respond (and the less useless output they produce) the better. Crashes are tolerable; hangs are problematic.

  • Tools need to be user friendly! Tools must clearly inform agents of misuse or errors to ensure forward progress.

  • Tools need to be protected against an LLM chaos monkey using them completely wrong. There is no such things as user error or undefined behavior!

  • Tools need to provide the right debuggability and observability.

Example: I place critical tools into a Makefile. A make dev command starts my services via a process manager. I made two key changes to what I normally do:

  1. I had to protect the process manager against being spawend a second time when it’s already running. I use a fork of shoreman which writes a pidfile. When spanwed a second time it errors and logs “services already running”. Why does that matter? Because the agent sometimes does not know if the server is already running and otherwise happily spawns it a second time resulting in two version of the service bound to the same port.

  2. I am not only printing the output to my terminal, I’m also always logging it to a file. This allows the agent to read the log file to diagnose what’s happening.

Here an example agentic interaction that demonstrates this rather well:

⏺ Great! The formatting looks good. Now let me run the development server to test the
   dynamic onboarding:

⏺ Bash(make dev)
  ⎿  Error: error: services are already running
     make: *** [dev] Error 1

⏺ The development server is already running. Let me check the logs to see the frontend URL:

⏺ Bash(make tail-log)
  ⎿ 01:06:55 backend    | Starting server on localhost:8080
    01:07:39 backend    | [Running: sh -c 'cd backend && go run cmd/server/main.go serve']
    01:07:39 backend    | Starting server on localhost:8080
    08:45:34 backend    | "GET <a href="http://localhost:8080/api/users/me" rel="nofollow">http://localhost:8080/api/users/me</a> HTTP/1.1" from 127.0.0.1:61804 - 200
    39.252666ms
    … +96 lines (ctrl+r to expand)

⏺ Perfect! I can see the frontend is running on <a href="http://localhost:3000/" rel="nofollow">http://localhost:3000/</a> and the latest changes
   have been hot-reloaded. Let me test the dynamic onboarding functionality using playwright:

In general logging is super important. For instance my app currently has a sign in and register flow that sends an email to the user. In debug mode (which the agent runs in), the email is just logged to stdout. This is crucial! It allows the agent to complete a full sign-in with a remote controlled browser without extra assistence. It knows that emails are being logged thanks to a CLAUDE.md instruction and it automatically consults the log for the necessary link to click.

Does it count as a tool? In my book it does.

It’s All About Speed

Agentic coding’s inefficiency largely arises from inference cost and suboptimal tool usage. Let me reiterate: quick, clear tool responses are vital. What we did not talk about yet is that some tools are “emergent,” temporarily written by agents themselves. Quick compilation and execution significantly boost productivity of the agent. So how can we help it?

With the right instructions it must be possible for the AI to create a new tool, by following existing conventions very quickly. This is necessary because you want the AI to write some new code, and run it. There is a big difference in the quality and speed of the flow, if that tool takes 3ms to run vs it compiles for 5 seconds and then needs another minute to boot and connect to database and kafka broker and 100 lines of nonsensical log output.

If your stuff is indeed slow, then consider vibe-coding a daemon that you can dynamically load stuff into. As an example Sentry takes too long to reload code and it takes too long to restart. To trial some agentic coding there my workaround was a module that watches a file system location and just imports and executes all python modules placed there, then writes the outputs into a log it can cat. That’s not perfect, but it was a significant help for the agent to evaluate some basic code in the context of the application.

Balancing log verbosity is crucial: informative yet concise logs optimize token usage and inference speed, avoiding unnecessary costs and rate limits. If you cannot find the balance, provide some easy to turn knobs for the AI to control.

In an idea setup you get useful log output as a natural byproduct of the agent writing code. Getting observability from the first shot of code generation beats writing code, failing to run it and only then going back to a debug loop where debug information is added.

Stability and Copy/Paste

Stable ecosystems are what you really want. LLMs are great with Go and they love to use Flask, because those are quite stable ecosystems with little churn. The same thing is true for your codebase. The AI likes to leave all kinds of breadcrumbs lying around when writing code that can turn into confusion later. For instance I have seen the agents leave useful comments about why it chose one path over another. If you nilly-willy let the AI upgrade libraries where some of those decisions no longer make sense, you now might have the AI continue making a now outdated pattern.

In theory this should be the same for agents and humans, but the reality is that agents make upgrades so “cheap” that it’s tempting to just let the AI do it and see if tests still pass. I do not find this to be a successful path at all. Be even more conservative about upgrades than before.

Likewise with AI I strongly prefer more code generation over using more dependencies. I wrote about why you should write your own code before, but the more I work with agentic coding, the more I am convinced of this.

Write Simple Code

Simple code significantly outperforms complex code in agentic contexts. I just recently wrote about ugly code and I think in the context of agents this is worth re-reading. Have the agent do “the dumbest possible thing that will work”.

  • Prefer functions with clear, descriptive and longer than usual function names over classes.

  • Avoid inheritance and overly clever hacks.

  • Use plain SQL. I mean it. You get excellent SQL out of agents and they can match the SQL they write with the SQL logs. That beats them min-maxing your ORM’s capabilities and getting lost in the SQL output in a log.

  • Keep important checks local. You really want to make sure that permission checks are very clear to the AI, and that they are taking place where it AI can see it. Hiding permission checks in another file or some config file will amost guarantee you that the AI will forget to add permission checks in when adding new routes.

Make It Parallelizable

Agents aren’t exceptionally fast individually, but parallelization boosts overall efficiency. Find a way to manage shared states like the file system, databases, or Redis instances so that you can run more than one. Avoid them, or find a way to quickly segment stuff out.

Your initial shared state is just the file system and a second check-out will do. But really I don’t have an amazing solution yet. There are some good initial attempts. For instance one of the tools to watch is container-use. It’s an MCP server that instructs Claude or other agents to run their experiments entirely in Docker.

Then there are tools like Cursor’s background agents and Codex which are moving this entire stuff into CI which will be interesting. So far, I don’t this is working for me yet, but let’s see again in a month.

Learn To Refactor

Agentic coding alters refactoring priorities. Agents handle tasks effectively until project complexity surpasses some manageable thresholds. Too big here is defined by the total amount of stuff that it has to consider. So for instance you can vibe code your frontend together for a while, but eventually you reach the point where you absolutely need to tell it to make a component library. Why? Because if the total tailwind class mess is splitered across 50 files you will find it very hard to get the AI to make redesigns or extract components without major regressions.

An agentic workflow encourages good code maintenance and refactoring at the right moment. You don’t want to do it too early and you definitely do not want to do it too late.

What Next?

Agentic coding is rapidly evolving, and my workflow today may look dramatically different tomorrow. What’s clear though is that integrating agents into your development process can unlock significant productivity gains. I encourage you to keep experimenting. The tools and techniques will evolve, but the core principles — simplicity, stability, observability and smart parallelization — will remain essential.

Ultimately, the goal is not just to leverage agents to write code faster, but to write better, more maintainable, and resilient code. Already today the code looks nothing like the terrible slop from a few months ago. Stay adaptable, and happy coding!

This entry was tagged ai and thoughts

copy as / view markdown

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Using Strategy Pattern with Dependency Injection in ASP.NET Core

1 Share

This blog post is originally published on https://blog.elmah.io/using-strategy-pattern-with-dependency-injection-in-asp-net-core/

Selection logic is a prominent part of many applications. Whether you add a simple environment toggle, a UI mode decision, or apply a discount, you have to rely on user input. Sometimes, simply using an intuitive if-else or a switch case can work. However, when conditions are growing or a complex algorithm selection is required, simple conditional statements can't work. Your code becomes exhaustive and hard to maintain. The Strategy pattern rescues the situation, adheres to the open/closed principle, and keeps the logic maintainable. This article walks you through a practical, straightforward example of the strategy pattern: choosing between Regular, VIP, and Student discount strategies at runtime.

Using Strategy Pattern with Dependency Injection in ASP.NET Core

What is the Strategy pattern?

The Strategy design pattern is a behavioral pattern used when you need to switch between different algorithms at runtime. The strategy pattern encapsulates algorithms and selects the right one when needed, usually based on an input. This pattern provides a flexible, maintainable solution to an algorithm-selection problem, keeping the code cleaner and easier to extend. If you need to add a new algorithm, just add another class instead of touching the existing logic, adhering to the open/closed principle.

What is the problem without the Strategy pattern?

To understand the usability of the strategy pattern, we need to identify the problems we may face without it. Suppose we offer different discounts to different users based on their membership. A naive solution is to use an if-else statement or a switch case. Let's do it and evaluate the implementation.

Step 1: Create a Console application

dotnet new console -n StrategyPatternDemo
cd StrategyPatternDemo

Step 2: Create DiscountService class

In the service, we will define discount calculation with a conditional statement.

public class DiscountService
{
    public decimal GetDiscount(string customerType, decimal amount)
    {
        if (customerType.ToLower() == "regular")
        {
            return amount * 0.05m;
        }
        else if (customerType.ToLower() == "vip")
        {
            return amount * 0.20m;
        }
        else
        {
            return 0;
        }
    }
}

Step 3: Use the service in the Strategy Pattern Sword.cs

using StrategyPatternDemo;

Console.Write("Enter customer type (regular/vip): ");
var type = Console.ReadLine();

Console.Write("Enter amount: ");
var amount = decimal.Parse(Console.ReadLine());

var service = new DiscountService();
var discount = service.GetDiscount(type, amount);
var final = amount - discount;

Console.WriteLine($"Discount: {discount}");
Console.WriteLine($"Final Price: {final}");

Step 4: Run and test

Let's test it

dotnet run

Output

Output

It works as expected. But the code contains design and maintainability flaws.

  • The solution violates the Open/Closed principle. Adding a new membership will require changes to the core method, such as adding an else-if block.
  • All the discount logic is tightly coupled in a single class and lacks separation of concerns or single responsibility.
  • Conjoined code makes testing harder. To ensure the functionality, you have to test the monster every time.
  • As the conditions grow, you can fall into a spiral of conditions. Imagine if you have 20 memberships, that will be a nightmare for maintainability.

Implementing the strategy pattern in a console application

In our example, let's address the above issues using the Strategy Pattern.

Step 1: Define Strategy Interface

Adding the discount strategy interface

public interface IDiscountStrategy
{
    decimal ApplyDiscount(decimal amount);
}

Step 2: Add concrete strategies

Adding separate implementations of each algorithm

public class RegularDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) => amount * 0.05m;
}

For Vip

public class VipDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) => amount * 0.20m;
}

Notice that none of the strategies implement validation or error handling. In real-world code, you would probably want to look into that. This part has been left out of this post since the focus is around splitting the business logic out in strategies.

Step 3: Define context class

public class DiscountService
{
    private readonly IDiscountStrategy _strategy;

    public DiscountService(IDiscountStrategy strategy)
    {
        _strategy = strategy;
    }

    public decimal GetDiscount(decimal amount) => _strategy.ApplyDiscount(amount);
}

The Context class in the strategy pattern holds a reference to a strategy interface (IDiscountStrategy in our case). It receives a strategy from outside. It does not implement logic itself, instead, it delegates work to the strategy, while the concrete classes define their logic.

Step 4: Use the strategy in the Program.cs


Console.WriteLine("Enter customer type (regular/vip): ");
string type = Console.ReadLine()?.ToLower();

IDiscountStrategy strategy;

// Manually picking strategy — no switch needed, but you *can* if you want.
if (type == "vip")
    strategy = new VipDiscount();
else
    strategy = new RegularDiscount();

var service = new DiscountService(strategy);

Console.Write("Enter amount: ");
decimal amount = decimal.Parse(Console.ReadLine());

var discount = service.GetDiscount(amount);
var finalPrice = amount - discount;

Console.WriteLine($"Discount applied: {discount}");
Console.WriteLine($"Final price: {finalPrice}");

Output

Output

We understand basic principles of the strategy pattern. We can proceed with our primary target: implementing the strategy pattern in ASP.NET Core.

Implementing the strategy pattern in an ASP.NET Core API

Step 1: Create a .NET Core api

Run the following command in the terminal

dotnet new webapi -n StrategyPatternApi
cd StrategyPatternApi

Step 2: Add concrete strategies

Adding separate implementations of each algorithm

public class RegularDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) => amount * 0.05m;
}

For Vip

public class VipDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) => amount * 0.20m;
}

Step 3: Define context class

public class DiscountService
{
    private readonly Func<string, IDiscountStrategy> _strategyFactory;

    public DiscountService(Func<string, IDiscountStrategy> strategyFactory)
    {
        _strategyFactory = strategyFactory;
    }

    // public API: ask for a discount by customer type
    public decimal GetDiscount(string customerType, decimal amount)
    {
        var strategy = _strategyFactory(customerType);
        return strategy.ApplyDiscount(amount);
    }
}

DiscountService plays the context role in the strategy pattern. DiscountService has a property Func<string, IDiscountStrategy> _strategyFactory that holds a factory delegate. The Func delegate returns an appropriate implementation of IDiscountStrategy based on the given type. Func allows the service to request a strategy at runtime by name/key without knowing the DI container internals or concrete types.

Step 4: Add a controller with the endpoint

[ApiController]
[Route("api/[controller]")]
public class PricingController : ControllerBase
{
    private readonly DiscountService _pricingService;

    public PricingController(DiscountService pricingService)
    {
        _pricingService = pricingService;
    }

    [HttpGet]
    public IActionResult Get([FromQuery] string type, [FromQuery] decimal amount)
    {
        var discount = _pricingService.GetDiscount(type, amount);
        var final = amount - discount;
        return Ok(new { type = type ?? "regular", amount, discount, final });
    }
}

Step 5: Configure Program.cs

Add the concrete services in dependency injection (DI) in the Program.cs file

services.AddTransient<RegularDiscount>();
services.AddTransient<VipDiscount>();

They are transient because discount strategies are stateless, so creating a new instance each time is fine. Note that I haven't injected them with IDiscountStrategy any implementing service because ASP.NET Core decides this automatically. Hence, the final code will look like this:

using StrategyPatternApi;

var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register concrete strategy types so they can be resolved by the factory
services.AddTransient<RegularDiscount>();
services.AddTransient<VipDiscount>();

services.AddSingleton<Func<string, IDiscountStrategy>>(sp => key =>
{
    var k = (key ?? "").Trim().ToLowerInvariant();
    return k switch
    {
        "vip" => sp.GetRequiredService<VipDiscount>(),
        // add more cases if you add more strategies
        _ => sp.GetRequiredService<RegularDiscount>()
    };
});

// Register the service that uses the factory
services.AddScoped<DiscountService>();

// Add controllers (or leave for minimal endpoints)
services.AddControllers();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.MapControllers();
app.Run();

In DI, the decisive part is:

services.AddSingleton<Func<string, IDiscountStrategy>>(sp => key =>
{
    var k = (key ?? "").Trim().ToLowerInvariant();
    return k switch
    {
        "vip" => sp.GetRequiredService<VipDiscount>(),
        // add more cases if you add more strategies
        _ => sp.GetRequiredService<RegularDiscount>()
    };
});

As explicitly stated, the switch condition resolves the appropriate concrete strategy via DI based on the type value. If any condition does not match, I made a default choice to get RegularService.

Step 6: Run and test

dotnet run

Now running the project

/api/Pricing endpoint
Response

Extension of algorithms in the ASP.NET Core strategy pattern

The Open/Close principle is one of the benefits of the Strategy Pattern. Let's continue with our example of how we can add a new discount within the bounds of the Open/Close principle.

Step 1: Add the Student discount's concrete strategy

public class StudentDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) => amount * 0.10m;
}

Step 2: Register a new service

services.AddTransient<StudentDiscount>();

Step 3: Update factory switch

services.AddSingleton<Func<string, IDiscountStrategy>>(sp => key =>
{
    var k = (key ?? "").Trim().ToLowerInvariant();
    return k switch
    {
        "vip" => sp.GetRequiredService<VipDiscount>(),
        "student" => sp.GetRequiredService<StudentDiscount>(),   
        _ => sp.GetRequiredService<RegularDiscount>()
    };
});

To add a new strategy implementation, we simply need to add the strategy code and inject it via dynamic DI.

Step 4: Run and test

dotnet run
/api/Pricing endpoint
Response

By default value

Default value
Response

Conclusion

Writing long if-else or cases is tiring. Every time you need to add a condition, you have to dive into the well and add one condition. The same happens while debugging. The strategy pattern provides a modular solution that keeps the code intact while dynamically allowing you to extend conditions. In this blog post, I highlighted the need for a strategy pattern and showed how to implement it in an ASP.NET Core API.

Example 1: https://github.com/elmahio-blog/StrategyPatternDemo

Example 2: https://github.com/elmahio-blog/StrategyPatternApi



Read the whole story
alvinashcraft
8 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Podcast: Platform Engineering for AI: Scaling Agents and MCP at LinkedIn

1 Share

QCon AI New York Chair Wes Reisz talks with LinkedIn’s Karthik Ramgopal and Prince Valluri about enabling AI agents at enterprise scale. They discuss how platform teams orchestrate secure, multi-agentic systems, the role of MCP, the use of foreground and background agents, improving developer experience, and reducing toil.

By Karthik Ramgopal, Prince Valluri
Read the whole story
alvinashcraft
8 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories