Never Use Service Classes in Rails

Service classes are one of the most representative anti-patterns in Rails.
That is because they deviate from Rails’ MVC principles.

You have probably heard of the Single Responsibility Principle.
In MVC, each component has its own role:

  • Model: storing, managing, processing, and relating data
  • View: display, templates, and user interface
  • Controller: handling HTTP requests

So, if we introduce a service class, what is its responsibility?
I do not know.

The name “service” creates the illusion that you can write some impressive-sounding business logic there.
However, the name “service” has no meaning or definition.
Why are so many people strict about meaningless variable names like tmp or x, yet willing to accept a meaningless class called a “service class”?

In my more than five years of experience with Ruby on Rails, service classes had unfortunately already been created in several projects.
In many of those cases, complex data-processing logic had been written in controllers, and service classes were used simply to clean up fat controllers.

That premise is wrong.
If you write business logic in models—especially logic related to retrieving and processing data—and keep controllers focused solely on calling that logic, controllers will never become complex.

“Do you accept fat models?”
Yes, of course.
In real companies, “business logic” is, in other words, a discussion about “how to handle data,” which is precisely the role of the model.
When business requirements are large, it is inevitable that models become complex and grow in size.

Implementing readable code within models is where we engineers show our skill.
There is never any place for service classes.

“What should you do if a model grows so large that a single file exceeds 1,000 lines?”
Please refer to the OSS code published by DHH and 37signals.
Even when we talk about fat models, that does not mean you should cram all logic into a single class.

GitHub logo

basecamp
/
once-campfire

Super simple group chat, without a subscription

Campfire

Campfire is a web-based chat application. It supports many of the features you’d
expect, including:

  • Multiple rooms, with access controls
  • Direct messages
  • File attachments with previews
  • Search
  • Notifications (via Web Push)
  • @mentions
  • API, with support for bot integrations

Deploying with Docker

Campfire’s Docker image contains everything needed for a fully-functional
single-machine deployment. This includes the web app, background jobs, caching,
file serving, and SSL.

To persist storage of the database and file attachments, map a volume to /rails/storage.

To configure additional features, you can set the following environment variables:

  • SSL_DOMAIN – enable automatic SSL via Let’s Encrypt for the given domain name
  • DISABLE_SSL – alternatively, set DISABLE_SSL to serve over plain HTTP
  • VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY – set these to a valid keypair to
    allow sending Web Push notifications. You can generate a new keypair by running
    /script/admin/create-vapid-key
  • SENTRY_DSN – to enable error reporting to sentry in production, supply…

For example, once-campfire defines the following files related to the User model:

  • models/user.rb
  • models/user/avatar.rb
  • models/user/bannable.rb
  • models/user/bot.rb
  • models/user/mentionalbe.rb
  • models/user/role.rb
  • models/user/transferable.rb

The first file, models/user.rb, is the one you are familiar with—the file created by rails g model User.
The others are modules that include ActionText::Attachable. In other words, they are concerns.

Even if something is not shared with other models, it should be extracted into a concern and split out.
That way, the main models/user.rb can remain simple.

By the way, who was it that started advocating for service classes in the first place?
DHH was loudly criticizing service classes, wasn’t he?

When I ask AI for advice on Rails refactoring, it forcibly suggests services even though I never asked for them.

It is extremely irritating.
It makes me realize that the expectation that AI will replace real engineers is nothing more than nonsense.

How Markus Builds AI Teams That Actually Ship — Not Just Chat

How Markus Builds AI Teams That Actually Ship — Not Just Chat

1. The ‘Alice in Wonderland’ Problem of LLMs

Large language models excel at conversation. Give one a question, and it returns a polished answer. Give it a code request, and it produces a working function. But ask it to build a feature, coordinate a code review, deploy to production, and report the outcome — and the illusion breaks.

This is the Alice in Wonderland problem of LLMs: strong at chatter, weak at delivery. A single AI agent can write code, but it cannot form a team. It cannot delegate a subtask to a specialist, review the result for quality, maintain context across a week-long project, or escalate a blocker to a human manager. The agent sits in a chat window, waiting for the next prompt — forever reactive, never proactive.

The industry response has been to build better tools. Agent frameworks, prompt chaining libraries, and LLM orchestrators all attempt to squeeze more capability out of a single agent. But the limit is not the agent. The limit is the organizational layer. A company of one — even a brilliant one — cannot match the throughput of a coordinated team with roles, governance, memory, and parallel execution.

Markus solves this problem by providing that organizational layer: an open-source AI workforce platform that runs complete AI teams, not just chat agents.

2. Problem: Single AI Agent Limitations

A single agent — whether Claude Code, Codex, ChatGPT, or any copilot — is effective at one task at a time. But single agents do not:

  • Coordinate. They cannot delegate subtasks to other agents or track dependencies across parallel workstreams.
  • Remember. Context evaporates when the session ends. Every new conversation starts from zero, even if the agent spent six hours on the same project yesterday.
  • Operate proactively. They wait for your prompt, every time. No agent checks on a long-running build or surfaces a blocker unless you explicitly ask.
  • Review each other. There is no quality gate between “agent said done” and “actually done.” The output of a single agent goes straight from LLM to user with no peer review.
  • Scale. Running ten agents means ten independent sessions with zero shared visibility. There is no dashboard, no task board, no unified view of what the team is doing.

These limitations are not fixable by improving the underlying LLM. They are structural. A single agent, no matter how capable, cannot be in two places at once. It cannot read its own output from a different context. It cannot enforce a review policy on itself.

The missing ingredient is an organizational layer — roles, teams, task boards, reviews, governance, persistent memory, and a dashboard that shows what every agent is doing. Markus provides exactly this layer.

3. Markus’s Solution: The Operating System for an AI Workforce

Markus is an open-source AI employee platform. It is not an agent framework or an LLM orchestrator. It is a platform for running AI companies.

The core differentiator between Markus and other approaches is three layers:

Layer What It Provides How It Works
Agent Runtime Full LLM-powered workers with built-in tools Each agent talks directly to LLM APIs (no proxying to external CLI tools), uses shell, file I/O, git, web search, code analysis, and MCP servers.
Team Layer Role-based collaboration with A2A protocol Agents delegate tasks, spawn subagents, send structured messages, and collaborate through a built-in Agent-to-Agent protocol. Managers route work, workers execute.
Governance Layer Progressive trust, formal delivery, audit trail Trust levels (probation → standard → trusted → senior) control autonomy. Submit-review-merge pipeline enforces quality gates. Every action is logged.

Markus includes the full agent runtime — it does not wrap external agent tools. Each agent is a complete worker with identity (ROLE.md), skills, proactive tasks (HEARTBEAT.md), behavioral rules, and persistent memory (MEMORY.md). The platform works with any LLM provider: Anthropic, OpenAI, Google, DeepSeek, MiniMax, Ollama, and more, with automatic failover between providers.

4. Core Technical Architecture

4.1 Three-Layer Memory System (Tulving)

Markus agents use a memory architecture based on Tulving’s cognitive classification:

Layer Storage Role
Procedural ROLE.md + skills How the agent operates. Identity, behavioral rules, tool permissions.
Semantic MEMORY.md + memories.json What the agent knows. Agent-organized knowledge, consolidated through the Dream Cycle.
Episodic sessions/*.json (current) + SQLite agent_activities (past) What happened. Current conversation context plus searchable activity history.

Memory persists across restarts, not just within a single conversation. The Dream Cycle runs periodically to consolidate memories, merge duplicates, and promote recurring patterns into curated knowledge. This means an agent that learned a project’s coding conventions on Tuesday applies that knowledge on Wednesday without being re-prompted.

4.2 Agent-to-Agent (A2A) Protocol

Agents communicate through a built-in A2A protocol. Any agent can send a structured message to any other agent. The message arrives in the target agent’s mailbox, is triaged by the Attention Controller, and is processed at the appropriate cognitive depth.

This enables a manager-worker architecture: a Manager agent delegates tasks to Worker agents, monitors progress, and handles escalations. Workers report blockers, request clarification, and submit deliverables — all through the A2A protocol.

4.3 Progressive Trust Levels

Markus implements progressive trust:

Trust Level Condition Permissions
probation New agent or score < 40 All tasks require human approval
standard Score ≥ 40, ≥ 5 deliveries Routine tasks auto-approved
trusted Score ≥ 60, ≥ 15 deliveries Higher autonomy, can review peers
senior Score ≥ 80, ≥ 25 deliveries Highest autonomy, key reviewer role

This creates a natural career progression that mirrors real engineering organizations.

4.4 Heartbeat Mechanism: Agents Work While You Sleep

Agents are not reactive. The HeartbeatScheduler drives periodic check-ins on a configured schedule. During each heartbeat, the agent:

  • Checks active tasks and updates stale states
  • Retries failed tasks
  • Processes background completion notifications
  • Saves insights and sends proactive notifications
  • Creates tasks for work that requires heavy implementation

This transforms an agent from a chat assistant into a proactive digital employee that works around the clock.

5. Submit-Review-Merge Pipeline

Every deliverable passes through a formal quality pipeline:

Agent completes work
  → task_submit_review (summary, branch, test results)
  → Quality gates (TypeScript build, ESLint, Vitest)
  → Merge conflict pre-check (dry-run merge)
  → Task state → review
  → Reviewer accepts or requests revision
  → Accept → merge branch → completed
  → Revision → agent reworks → resubmit

This pipeline guarantees that no code reaches “completed” without passing TypeScript compilation, ESLint checks, and Vitest tests. The merge conflict pre-check runs a dry-run merge before the reviewer even sees the submission.

6. Comparison: Markus vs. Alternatives

Factor LangChain Agents / CrewAI / AutoGen Markus
Runtime Orchestrator with external CLI tools Full embedded agent runtime with built-in tools
Memory Session-scoped or minimal Three-layer persistent memory (Tulving model)
Proactivity Reactive — waits for user input Heartbeat-driven, works autonomously
Governance None or minimal Progressive trust, submit-review-merge, audit trail
Team model Manual orchestration code A2A protocol, subagent spawning, manager/worker roles
Quality gates None TypeScript, ESLint, Vitest enforced per submission
Observability CLI logs per agent Centralized dashboard, real-time WebSocket events, full activity history

CrewAI and AutoGen provide valuable building blocks for multi-agent conversations. But they remain agent frameworks — they give you the components to build a multi-agent system. Markus is an agent platform — it gives you the running system, complete with governance, memory, collaboration protocols, and a delivery pipeline that enforces quality.

7. Conclusion: Why Markus Is Different

Markus is open source (AGPL-3.0) and installs with a single command:

curl -fsSL https://markus.global/install.sh | bash

No Docker. No PostgreSQL. No Go compiler. SQLite database, bundled web UI, zero external dependencies. Deploy it on a cloud server and manage your entire AI workforce from your phone.

The age of single-agent chat is over. The age of AI teams is here.

Get started on GitHub →

Follow the Markus project for more deep dives into AI agent architecture, multi-agent system design, and open-source AI workforce engineering.

Pricing logic feels boring until it’s wrong.

Build it like infrastructure from day one.
Most pricing engines are built wrong. Here’s what I’d do instead.
If you’re building a CPQ product — or any SaaS tool where users configure and price deals — your pricing logic is probably living in the wrong place.
It’s in a component. Or a utility function called from three different places. Or worse, duplicated between your frontend table renderer and your backend invoice service, silently drifting apart until a customer notices the numbers don’t match.
I’ve thought a lot about how to architect this properly. Here’s what I’d do.

First: treat pricing as infrastructure, not a feature
The moment you have billing frequency, line-item discounts, currency formatting, and tax rules composing together, you don’t have a utility anymore. You have a domain. It deserves its own package, its own test suite, and its own ownership.
A shared @your-org/pricing-engine package — published internally, consumed by your frontend, your backend, and your export pipeline — means one place where

unitPrice × quantity × frequency = subtotal

is defined. Not three.
Why does this matter in practice? Consider this scenario:
A sales rep quotes a client €90/month per seat for a SaaS tool, after a 10% discount, converted from USD. That number needs to be:

  • Identical in the live proposal table the rep is editing
  • Identical in the PDF the client downloads and signs
  • Identical in the invoice the billing system generates on day one
  • Identical in the revenue report the finance team pulls at month end

If your frontend, your PDF export service, and your billing backend each have their own implementation of applyDiscount() and convertCurrency(), you will eventually have a discrepancy. Not maybe. Eventually.

Never use floating-point arithmetic for money. Ever.

0.1 + 0.2 === 0.30000000000000004

This isn’t a JavaScript quirk. It’s IEEE 754 — the way binary floating-point works at a hardware level. And it will silently corrupt your customer invoices.
A real example of how this surfaces: a pricing table with 7 line items, each with a percentage discount and a currency conversion applied. By the time you sum those rows, the float drift compounds. Your table shows $1,200.00. Your invoice says $1,199.99. Your customer notices. Your support team gets a ticket. Your engineers spend a day debugging something that was never going to work correctly.
Use decimal.js or equivalent to it. Treat it as a hard rule, not a code style preference. Decimal arithmetic is slower — negligibly so at any realistic pricing table scale. There is no valid argument for floating-point in customer-facing money calculations.

Keep your engine pure
A pricing engine should be a pure function:

inputs → outputs

No Redux. No React. No HTTP calls. No side effects. Just data in, calculated data out.
What the engine looks like from the outside

const result = computeTablePricing({
  rows: [
    {
      id: "row-1",
      unitPrice: new Decimal("100.00"),
      quantity: new Decimal("5"),
      billingFrequency: "monthly",
      discount: { type: "percentage", value: new Decimal("10") }
    }
  ],
  currency: { code: "EUR", symbol: "€", symbolPosition: "front", decimalPlaces: 2 },
  settings: { taxRate: new Decimal("0") }
});

// result.rows["row-1"].subtotal === Decimal("450.00")
// result.frequencyTotals.monthly === Decimal("450.00")
// result.grandTotal === Decimal("450.00")

This matters more than it sounds. A pure engine:

Runs identically on web, server, React Native, and in a unit test
Can be validated against your old implementation in shadow mode before touching production
Can be reasoned about without understanding your component tree
Can be tested with plain input/output assertions — no mocking, no rendering, no Redux store setup

The billing frequency problem — a concrete example of why this matters
Let’s say you want to add billing frequency to your pricing table. Line items can be one-time, monthly, or annual. The footer should show a subtotal per frequency group.
In a component-centric architecture, you add grouping logic to your table renderer. Then you realize your PDF export also needs frequency subtotals, so you add it there too. Then billing needs it. Three implementations. Three places to get out of sync.
With an engine, billing frequency is just a first-class input field on each row:

billingFrequency: 'one-time' | 'weekly' | 'monthly' | 'quarterly' | 'annual'

The engine computes frequency subtotals as outputs. The table reads them. The PDF reads them. The billing service reads them. Same numbers everywhere, because it’s the same function.

The discount composition problem — where things really break
Discounts are where scattered pricing logic becomes a genuine product risk.
Consider what a real CPQ discount model looks like:

A rep can apply up to 10% at line-item level without approval
Anything above 10% needs manager sign-off
There's a volume discount: 5+ seats get an additional 5% off
There's a seasonal promotion: 15% off annual plans in Q4
These can stack — but only in specific combinations

If discount logic lives in your table component, how does your catalog apply the same rules? How does your billing service validate that the agreed discount is still applied at invoice time? How do you write a unit test for the approval threshold without rendering a table?
The answer is: you can’t, cleanly. You end up with discount logic scattered across five files, each with slightly different behavior.
In an engine architecture, discount application is a shared rule:

// shared/discount-application.ts
export function applyDiscount(
  baseAmount: Decimal,
  discount: { type: 'percentage' | 'fixed'; value: Decimal }
): { discountedAmount: Decimal; discountAmount: Decimal } {
  const discountAmount =
    discount.type === 'percentage'
      ? baseAmount.mul(discount.value).div(100)
      : discount.value;

  return {
    discountedAmount: baseAmount.minus(discountAmount),
    discountAmount
  };
}

One function. Tested once. Used by the table calculator, the catalog calculator, the billing service, and the approval workflow validator. Identical behavior everywhere by definition.

The feature composition problem — this is the real ceiling
Individual features are manageable. The problem is when they compose.
A line item that has:

Billing frequency: monthly
Discount: 10% off
Currency: EUR, converted from USD at current rate
Tax: 20% VAT applied after discount

…needs to produce exactly the same number in your live editor, your PDF, your invoice, and your analytics pipeline. Every combination of features multiplies the number of cases where scattered logic can diverge.
This is what actually blocks CPQ roadmaps. Not any individual feature — the combinatorial explosion of feature interactions when your calculation logic is spread across the codebase.

Let your state layer stay clean
In a Redux architecture, the pattern that actually works:

Redux stores confirmed inputs only (what the user committed — prices, quantities, discounts, frequencies)
Selectors derive all calculated values by passing those inputs through the engine
Components never calculate — they only display

// selector reads inputs from Redux, derives outputs via engine
const selectTablePricingResult = createSelector(
  [selectTablePricingInputs],
  (inputs) => computeTablePricing(inputs) // pure function, memoized automatically
);

This eliminates an entire class of bugs where your displayed subtotal disagrees with what gets saved, invoiced, or exported. The Redux store is always clean data. Derived values are never persisted. There is no ambiguity about whether a stored subtotal field is a user input or a computed value — a distinction that causes real bugs in collaborative editing and undo/redo flows.

The migration play — how to get there without breaking production
If you’re refactoring an existing system rather than greenfielding, shadow mode is your best friend.
Run the new engine in parallel with your old implementation. For every calculation your old code produces, the engine produces the same calculation independently. Log any divergence. Ship zero user-facing changes until the outputs match exactly — across every row type, every discount combination, every currency, every edge case you can throw at it.
Silent arithmetic regressions on customer invoices are not a recoverable situation. Shadow mode gives you mathematical certainty before you flip the switch.
The sequencing I’d recommend:

Engine package first — pure functions, no UI changes, full test coverage
New features through the engine — billing frequency, discounts, anything net-new goes through the engine from day one, minimizing regression risk on existing behavior
Shadow mode on existing features — validate the engine matches current behavior exactly
Migrate existing consumers — replace old calculations one surface at a time, behind a feature flag
Remove the old code — only after full coverage and monitoring confirms correctness

What this unlocks long-term
Once you have a pure, shared pricing engine, a lot of things that were hard become straightforward:

Backend adoption: Your invoice service imports the same npm package your frontend uses. Calculation discrepancies between frontend and backend become structurally impossible
Mobile: React Native consumes the same engine. No separate mobile pricing logic
Analytics: Revenue reports use the same calculation rules as the proposals that generated the revenue
Formula engine: User-programmable pricing formulas

(=quantity * unitPrice * (1 - discount))

become an extension of the engine, not a rewrite of it.

The point
Pricing logic feels boring until it’s wrong. Build it like infrastructure from day one — pure, shared, tested, and decoupled from your UI layer. The payoff isn’t visible immediately. It’s visible when your fifth pricing feature composes correctly with your first four without a single edge case meeting.
Have you tackled pricing complexity at scale? Curious what patterns have worked — especially around currency and tax.

Software Engineer Skills Companies Want in 2026: 48K-Posting Analysis

Skill families in Software Engineer postings: Coding Languages 72%, Tools and Infrastructure 61%, Cloud Platforms 34%, Querying and SQL 33%, Process and Methodology 31%, Data Engineering Foundations 20%, Machine Learning and AI 16%

Why Does the Software Engineer Title Mean So Many Different Things in 2026?

If you scrape every “Software Engineer” posting on the open web in 2026, the most surprising finding is what’s missing: a single skill that every job asks for. Python tops the list, but only at 34.8% of postings. Nothing else clears that. There is no skill in the Software Engineer market that plays the role SQL plays for Data Analysts or Python-plus-pipelines plays for Data Engineers, no universal filter the resume must clear.

That absence is the story. “Software Engineer” in 2026 is less a single role than a job-family label, and the data reflects it: a frontend engineer at a SaaS startup and a principal embedded engineer at a defense contractor both wear the title, and so do the ML platform engineer at a self-driving company and the back-office Java developer at a bank. The result is a fragmented stack, a polyglot hiring posture, and real money paid to candidates who specialize without losing the breadth.

To put numbers on it, we looked at every active Software Engineer posting on the InterviewStack.io job board as of May 2026: 48,207 listings, with skills extracted from descriptions and synonyms collapsed (so etl and data pipelines count once, gcp and google cloud count once). It is the largest single-role dataset we have analyzed.

Key findings

  • 48,207 active Software Engineer postings analyzed across the live job board as of May 2026: the largest single-role dataset in our coverage.
  • No skill clears the 50% table-stakes line. Python leads at 34.8%, followed by Agile (29.8%), Java (27.3%), AWS (26.5%), and CI/CD (26.3%).
  • The polyglot reality is in the data: Java (27.3%), JavaScript (19.2%), TypeScript (17.7%), C++ (12.3%), and C# (12.0%) all appear in roughly one in eight to one in four postings.
  • Median US base salary is $140,000 (n=10,765), about $12K above the Data Engineer median and $53K above the Data Analyst median.
  • Specialization pays $19K to $30K above baseline for the top tier: Deep Learning, Computer Vision, Rust, dbt, Distributed Systems, Observability, gRPC, BigQuery, and Apache Spark all sit at $159K-$170K (n at least 75 each). A second tier of $9K to $17K premiums adds C++, Go, System Design, Next.js, Snowflake, Scala, GraphQL, and PyTorch.
  • Docker + Kubernetes is the strongest pair in the dataset at lift 3.75: about 70% of postings that mention Docker also ask for Kubernetes.
  • Only 4.0% of postings are entry-level (1,915 of 48,207); senior plus staff together are 44% of the market.
  • The US is 37.4% of postings, India 19.1%, and onsite remains the dominant work mode at 62.3% (hybrid 27%, remote 18%).

What Skill Families Define a Software Engineer Role in 2026?

Group every individual skill into the higher-level family it belongs to and count how many postings ask for at least one skill in that family. The picture is wide and shallow rather than narrow and deep.

Skill families in Software Engineer postings: Coding Languages 72%, Tools and Infrastructure 61%, Cloud Platforms 34%, Querying and SQL 33%, Process and Methodology 31%, Data Engineering Foundations 20%, Machine Learning and AI 16%

Share of Software Engineer postings that ask for at least one skill in each family. A posting that mentions both Python and Java counts once under “Coding Languages”.

The families that actually define the role:

  1. Coding Languages: 72% of postings (Python, Java, JavaScript, TypeScript, C++, C#)
  2. Tools & Infrastructure: 61% (automation, Git, Docker, Kubernetes, monitoring, Terraform)
  3. Cloud Platforms: 34% (AWS, Azure, Google Cloud)
  4. Querying & SQL: 33% (SQL itself, PostgreSQL, NoSQL, MySQL)
  5. Process & Methodology: 31% (Agile, Scrum)
  6. Data Engineering Foundations: 20% (Kafka, data pipelines)
  7. Machine Learning & AI: 16% (machine learning, deep learning, generative AI)

Read the families against the Data Engineer post and the contrast is sharp. For Data Engineer, Data Engineering Foundations sits at 89% and Querying & SQL at 74%, because the role really is a stack. For Software Engineer, Coding Languages tops out at 72% and no other family clears two-thirds. The role does not converge on one thing.

The 34% cloud-platforms share is a useful pivot. Two-thirds of Software Engineer postings name no specific cloud at all, which fits the breadth of the title: client-side code, embedded systems, internal tooling, and on-premise enterprise work all still exist. But the postings that do name a cloud overwhelmingly cluster around AWS, with Azure and Google Cloud following.

What Are the Three Tiers of Individual Software Engineer Skills?

Drill into individual skills inside those families and three tiers emerge. The catch: the top tier is empty.

Top individual skills color-coded by tier: Python 34.8%, Agile 29.8%, Java 27.3%, AWS 26.5%, CI/CD 26.3%, Automation 24.0%, SQL 21.9%, APIs 21.1% sit in the common tier; JavaScript 19.2%, Kubernetes 18.6%, TypeScript 17.7%, Docker 17.2%, Monitoring 16.2%, Git 16.1%, Azure 16.1%, React 15.2%, Scalability 14.5%, Microservices 14.0%, Distributed Systems 13.2% are differentiators

Top individual skills in Software Engineer postings, by share of listings that mention them. Skills above 50% would be table stakes; 20-50% are common; 5-20% are differentiators. No skill in this dataset clears the table-stakes threshold.

Table Stakes (50%+ of postings)

Empty. No individual skill appears in the majority of Software Engineer postings. The headline number is that Python, the most-demanded skill, shows up in just over a third of listings (34.8%). For comparison, Data Engineer postings have three skills above 70%, and Data Analyst postings have SQL above 80%. The Software Engineer market does not have a single hard filter.

What does that mean practically? It means there is no skill on a Software Engineer resume that screens you out of the majority of roles by being absent. It also means there is no skill that screens you in. Hiring filters happen lower in the stack: a Java backend posting filters for Java specifically, a React frontend role filters for React, an ML platform team filters for PyTorch. The role-level dataset shows the union, not the intersection.

Common Expectations (20-50% of postings)

This is where the role’s center of gravity lives.

Two patterns jump out. First, Python has overtaken Java at the role level. Java has historically dominated enterprise SWE postings, but in this snapshot Python sits seven points ahead, pulled up by data tooling, ML platforms, internal scripting, and DevOps automation work that all sit under the “Software Engineer” umbrella. Java is still the second language of the role, not the first.

Second, Agile at 29.8% is the only soft-process skill in the common tier. That is high compared to most data roles and reflects how much SWE work happens in scrum-team org structures. Agile is a credentialing keyword in this market; calling it out on a resume is rarely wrong.

Differentiators (5-20% of postings)

This is where specialization shows up and where the salary curve starts to bend.

  • JavaScript: 19.2%
  • Kubernetes: 18.6%
  • TypeScript: 17.7%
  • Docker: 17.2%
  • Monitoring: 16.2%
  • Git: 16.1%
  • Azure: 16.1%
  • Debugging: 15.7%
  • React: 15.2%
  • Scalability: 14.5%
  • Microservices: 14.0%
  • Distributed Systems: 13.2% (Software Engineer + Distributed Systems openings)
  • Linux: 12.8%
  • C++: 12.3%
  • C#: 12.0%
  • Google Cloud: 11.8%
  • Observability: 11.3%
  • PostgreSQL: 10.9%
  • Testing: 10.8%
  • Nodejs: 8.7%
  • Kafka: 7.8%
  • System Design: 7.3%
  • Terraform: 6.9%
  • Machine Learning: 6.7%
  • LLMs: 5.0%

The differentiator tier is dense with cloud-native infrastructure skills (Kubernetes, Docker, Terraform, monitoring, observability) and with architecture concepts (microservices, distributed systems, system design, scalability). These are the skills that move a Software Engineer from “writes the feature” to “owns the system”, and as the salary section shows next, the market pays for that distinction.

The frontend tier (React, TypeScript, JavaScript) sits in the same band, with TypeScript at 17.7% nearly matching JavaScript at 19.2%, evidence that the typed-frontend default has won. LLMs at 5.0% are the newest entrant on the differentiator list, sitting just above the 5% noise floor. They show up specifically in postings asking the engineer to build retrieval pipelines, evaluation harnesses, or LLM-backed product features rather than train foundation models.

Which Software Engineer Skills Pay More Than the Baseline?

Salary numbers below are restricted to US postings only (where wage-transparency laws produce consistent disclosure) so they’re directly comparable. The numbers are base salary: equity, RSUs, bonuses, and sign-on are not disclosed in postings, so total compensation at top employers is meaningfully higher than what we report here, especially in tech and finance.

The overall median US base salary for Software Engineer postings is $140,000 (n=10,765). That sits about $12,000 above the Data Engineer median ($128,300) and about $53,000 above the Data Analyst median ($87,200). The SWE premium reflects a deeper coding bar and broader architectural responsibility, not a different geography mix.

Median US base salary by skill for Software Engineer postings: top earners include Deep Learning, Computer Vision, Rust, dbt, Distributed Systems, Observability, gRPC, BigQuery, Apache Spark, PyTorch, GraphQL, Scala, LLMs, System Design, Snowflake, Next.js, Go, and C++

Median US base salary in USD for postings that mention each skill, among US Software Engineer postings with structured salary data.

The biggest premiums attach to research, infrastructure, and modern-stack specialties. Skills with premiums of roughly $26K to $30K above the $140,000 baseline:

  • Deep Learning: $170,000 (n=182)
  • Computer Vision: $166,000 (n=414)

Skills with premiums of roughly $20K to $24K:

  • Rust: $164,400 (n=578)
  • dbt (a SQL transformation framework that runs inside the data warehouse): $163,800 (n=77)
  • Distributed Systems: $160,000 (n=2,017)
  • Observability: $160,000 (n=1,513)
  • gRPC (a high-performance remote-procedure-call framework used between backend services): $160,000 (n=291)
  • Datadog: $160,000 (n=233)
  • Incident Response: $160,000 (n=309)
  • Design Systems: $160,000 (n=244)
  • BigQuery: $160,000 (n=100)
  • Apache Spark: $159,200 (n=565)

Skills with premiums of roughly $13K to $17K:

  • PyTorch: $157,000 (n=237)
  • OpenAI: $155,300 (n=210)
  • Databricks: $155,000 (n=197)
  • Machine Learning: $153,100 (n=1,052)
  • GraphQL: $153,000 (n=399)
  • Scala: $152,800 (n=273)
  • LLM: $152,400 (n=577)

Smaller premiums of about $9K to $11K:

  • Llms: $151,000 (n=582)
  • Airflow (the open-source orchestrator most data teams use to schedule pipelines): $150,300 (n=253)
  • System Design: $150,000 (n=918)
  • Snowflake: $150,000 (n=251)
  • Next.js: $150,000 (n=252)
  • Go: $149,100 (n=422)
  • C++: $149,000 (n=1,841)
  • Data Pipelines: $149,000 (n=1,036)

Three patterns are worth naming. First, the AI/research stack pays the largest premiums. Deep Learning, Computer Vision, PyTorch, OpenAI, LLMs, and Machine Learning all sit $13K to $30K above baseline, which is the strongest signal in the dataset that companies are competing for engineers who can ship AI-adjacent product work. Second, the infrastructure stack still pays well: Distributed Systems, Observability, gRPC, and Apache Spark each sit at $160K, $20K above baseline. Third, the languages premium has shifted: Rust ($164.4K) and Go ($149.1K) are now the languages that move the needle, while traditional enterprise stacks (Java at $135K, C# at $124K) sit below the SWE baseline because they correlate with onshore-enterprise and global-services postings rather than the highest-paying tech segments.

The widely-asked skills sit close to baseline because they are the baseline. Python (n=4,867), TypeScript (n=2,568), AWS (n=3,115), Kubernetes (n=2,387), APIs (n=2,496), and React (n=1,724) all median exactly at $140,000. They are price-of-entry, not differentiators.

The practical takeaway: building the common-tier skills (Python or Java, AWS, CI/CD, SQL) keeps you in the running. Adding one differentiator that maps to a hiring concentration (Rust, distributed systems, observability tooling, or an AI specialty like PyTorch or LLM application work) is what moves your median offer by $20K or more. Our interview-prep courses cover the foundations across coding interviews, system design, and DSA; the question bank is where you drill the specific topics that come up in onsite rounds.

What Is the Dominant Software Engineer Skill Stack?

We computed every two-skill co-occurrence among the top 25 skills to find the combinations that show up together more often than chance.

The strongest pairs by lift, where lift greater than 1 means the two skills appear together more often than their individual frequencies would predict:

Skill pair Postings that mention both % of postings Lift
Docker + Kubernetes 5,777 12.0% 3.75
AWS + Google Cloud 4,519 9.4% 2.99
AWS + Azure 5,277 10.9% 2.57
AWS + Kubernetes 5,387 11.2% 2.27
AWS + Microservices 4,009 8.3% 2.24
AWS + Docker 4,868 10.1% 2.21
CI/CD + Docker 4,618 9.6% 2.11
CI/CD + Kubernetes 4,556 9.5% 1.93
AWS + CI/CD 5,942 12.3% 1.76
Java + Kubernetes 4,065 8.4% 1.67

Each pair tells you something concrete about how postings actually compose skills:

  • Docker + Kubernetes (lift 3.75) is the strongest pair in the entire dataset, by a wide margin. About 7 in 10 postings that mention Docker also ask for Kubernetes, and most postings that mention Kubernetes ask for Docker too. The two are effectively treated as one skill at the hiring filter, the container-orchestration competence.
  • AWS + Google Cloud (lift 2.99) and AWS + Azure (lift 2.57) confirm that multi-cloud is a real hiring signal, not a buzzword. About 4 in 10 postings that name AWS also name Azure, and about 1 in 3 also name Google Cloud. Companies are asking for engineers who can move between providers, especially in enterprise and consulting work.
  • AWS + Kubernetes (lift 2.27), AWS + Microservices (lift 2.24), and AWS + Docker (lift 2.21) sketch the modern cloud-native backend cluster. If you are building a stack around AWS, the postings overwhelmingly assume you know the container and microservices layer that sits on top of it.
  • Python + SQL (lift 1.10, 4,034 postings) is barely above chance, a sharp contrast with Data Engineer postings where the pair has a lift of 1.15 and covers 58% of the market. For Software Engineers, the SQL-plus-Python base is just one of many bases.
  • Agile + Python (lift 0.89) is the dataset’s most notable anti-correlation. Python-heavy postings are slightly less likely to mention Agile than chance would predict, evidence that Python-leaning postings concentrate in ML, research, and platform work where the Agile-keyword vocabulary is less common than in mainstream enterprise dev.

The big pattern: companies want a base layer (a language plus a cloud) and an operations layer (Kubernetes plus Docker plus CI/CD plus monitoring) and either a depth specialty (distributed systems, observability, AI/ML) or a stack specialty (frontend, mobile, embedded). The “language plus IDE” world of an earlier era does not exist in 2026 SWE hiring.

Who’s Hiring at Which Seniority Level?

We tagged each posting’s seniority based on title keywords (Senior, Lead, Principal, Junior, Intern). Postings with no explicit signal default to mid-level.

Seniority mix for Software Engineer postings: 52% mid-level, 30% senior, 14% staff or lead, 4% entry

Seniority distribution of Software Engineer postings.

Two things stand out. First, only 1 in 25 Software Engineer postings is explicitly entry-level. The market still hires juniors, but it does so under different titles (Junior Developer, Associate Engineer, Intern, internal training programs), so the “Software Engineer” label itself skews experienced. New-grad job seekers should expect to compete for a narrower share of postings than the dataset’s headline suggests and should widen their search to Associate or Junior titles.

Second, the senior-and-above tiers (senior plus staff) make up 44% of postings, one of the deepest senior pipelines in tech. There is real career runway on the IC track, and the differentiator skills (distributed systems, system design, observability, multi-cloud) become required rather than nice-to-have once you cross into senior territory. Mid-to-senior is the largest single transition the dataset surfaces, and the salary curve bends there.

Where Are Software Engineer Jobs Located, and How Remote-Friendly Are They?

Geography for Software Engineer roles is more US-concentrated than for Data Engineer roles, where India and the US were nearly tied. The SWE market is still primarily a US market.

Geography of Software Engineer postings: US 37%, India 19%, Canada 4%, UK 3%, Germany 3%, Poland 1.5%, Australia 1.4%, Singapore 1.2%, Mexico 1.1%

Top countries by share of Software Engineer postings.

The US has roughly twice the share of any other country, which is unusual: most other tech roles we have analyzed cluster the US and India closer together. The likely explanation is that the Software Engineer title in India is split across more local-language variants (Software Developer, Software Engineer L1/L2, Application Developer), so the resolved-role filter picks up fewer of them. Even so, India remains the second-largest single market by a wide margin.

The remote-first assumption holds less for Software Engineers than the headlines suggest.

Work mode mix for Software Engineer postings: 62% onsite, 27% hybrid, 18% remote, some postings tagged with multiple modes

Share of Software Engineer postings tagged with each work mode. Some postings carry multiple tags (e.g., “Hybrid or Remote”), so percentages sum to more than 100%.

Postings can carry multiple work-mode tags when a company says “Hybrid or Remote”, which is why the percentages sum to more than 100%. The takeaway: nearly 2 in 3 Software Engineer postings still default to onsite, lower than other tech roles we have analyzed. The dataset is heavy with defense, aerospace, chip-design, and financial-services employers (visible in the top-companies list below), and those segments have remained onsite-first even as product SaaS has gone remote. The fully-remote share is concentrated in product tech and SaaS; if remote work is a hard requirement for your search, expect to filter down to roughly the same 17.5% of the market.

Who’s Hiring Software Engineers in 2026?

The top hiring companies on our board span global consulting, GPU and chip design, defense and aerospace, banking, and product SaaS, a more diverse mix than any other tech role we have surveyed.

Top hiring companies for Software Engineers: Accenture 3126, Speechify 822, NVIDIA 438, Anduril 269, Boardroom Appointments 248, Cisco 243, AgileEngine 243, Softtest Pays 220, Northrop Grumman 216, Cadence Design Systems 213, PradeepIT 211, Leidos 206

Top companies by active Software Engineer postings. Counts include all locations of the same job.

  • Accenture: 3,126 postings (global consulting)
  • Speechify: 822 (consumer AI / accessibility)
  • NVIDIA Corporation: 438 (GPU and AI infrastructure)
  • Anduril Industries: 269 (defense technology)
  • Boardroom Appointments: 248 (staffing)
  • Cisco: 243 (networking and security)
  • AgileEngine: 243 (software services)
  • Softtest Pays: 220 (QA and services)
  • Northrop Grumman: 216 (defense and aerospace)
  • Cadence Design Systems: 213 (chip-design EDA)
  • PradeepIT: 211 (staffing)
  • Leidos: 206 (defense and government IT)
  • RELX Group: 188 (information services)
  • Mastercard: 174 (payments)
  • SpaceX: 166 (aerospace)
  • Autodesk: 155 (engineering software)
  • Barclays: 155 (banking)
  • Exadel: 151 (software services)

A few specific takeaways. Accenture’s lead is enormous: at 3,126 postings, the consulting firm is hiring nearly 4 times as many Software Engineers as the next-largest employer. Defense and aerospace is unusually well-represented for a SWE list (Anduril, Northrop Grumman, Leidos, SpaceX), reflecting the post-2024 buildup in defense-tech and space launch. Chip-design and GPU work shows up next (NVIDIA, Cadence), driven directly by the AI hardware build-out. The mainstream-tech companies you might expect to see at the top are present but smaller in posting volume than the consulting and defense players.

For specific company processes, our interview preparation guides break down the rounds, topic priorities, and behavioral expectations company by company.

How to Use This in Your Job Search

If you are preparing for a Software Engineer job hunt, the data points to a clear sequence.

1. Pick your stack, not just your skills. Because no skill is table stakes at the role level, the filter you actually face is at the stack level. A Java backend hire wants Java specifically. An ML platform team wants Python plus PyTorch plus distributed systems. A frontend role wants React plus TypeScript plus a design-system fluency. Pick the stack that matches the companies you actually want to work for, then optimize for that stack rather than for the union of every skill in the dataset.

2. Lock down the cloud-native cluster. Docker + Kubernetes is the strongest pair in the data, and AWS pairs strongly with both. If you are targeting backend or platform roles, the cloud-native cluster (Docker, Kubernetes, AWS or another cloud, CI/CD, observability) is what hiring managers assume you can speak to, even if no single skill is named in every posting.

3. Add one differentiator that moves the salary curve. The salary data is unambiguous: the skills that pay $20K or more above baseline are the differentiators, not the table stakes. Distributed Systems, Rust, gRPC, Apache Spark, observability tooling, and AI/ML specialties (PyTorch, LLMs, Machine Learning) all move your median US base salary by $13K to $30K. Pick one that fits the kind of system you want to build and learn it deeply enough to talk through trade-offs in an onsite.

4. Drill the topics, then practice the rounds. Reading about Software Engineer skills is easy; performing under interview conditions is the hard part. Our interview-prep courses cover the foundations across DSA, system design, and coding interviews. The question bank lets you drill specific topics (system design, distributed systems, concurrency, algorithms) one at a time. AI mock interviews let you practice the full round under realistic conditions, with on-demand feedback on coding and system-design questions specifically.

5. Filter the job board for your stack. Browse current Software Engineer openings on the InterviewStack.io job board and combine role and skill filters to narrow to your exact stack. For example, Software Engineer + Rust for systems work, or Software Engineer + Kubernetes + AWS for cloud-native backend. The board updates daily, so the listings are current.

FAQ

Q. What skills do companies want for Software Engineer roles in 2026?

No single skill is table stakes. The role is unusually fragmented across stacks. Python leads at 34.8% of postings, followed by Agile (29.8%), Java (27.3%), AWS (26.5%), CI/CD (26.3%), Automation (24.0%), SQL (21.9%), and APIs (21.1%). Below that, JavaScript, Kubernetes, TypeScript, and Docker each appear in 17-19% of postings. The pattern is polyglot: companies want fluency across languages, cloud, and DevOps rather than mastery of one stack.

Q. What is the median Software Engineer salary in 2026?

The median US base salary across 10,765 Software Engineer postings with disclosed US salary is $140,000. That figure excludes equity, bonuses, RSUs, and sign-on, so total compensation at top employers (especially in tech and finance) runs meaningfully higher than the base.

Q. Which Software Engineer skills pay the highest premium over the role baseline?

Among US postings, the largest premiums attach to research, infrastructure, and modern-stack specialties. Deep Learning ($170K, n=182) and Computer Vision ($166K, n=414) each sit $26K to $30K above the $140,000 baseline. Rust ($164.4K, n=578) and dbt ($163.8K, n=77) clear roughly $24K. Distributed Systems ($160K, n=2,017), Observability ($160K, n=1,513), gRPC ($160K, n=291), and BigQuery ($160K, n=100) each pay about $20K above baseline. PyTorch, GraphQL, Scala, LLMs, System Design, Snowflake, Next.js, Go, and C++ follow with premiums in the $9K to $17K range.

Q. Is Software Engineer a good entry-level role to break into?

Only 4.0% of Software Engineer postings are explicitly entry-level (1,915 of 48,207). That is higher than the 3% entry-level share in Data Engineer hiring, but still narrow. Mid-level (52.1%) and senior-or-above (44%) roles dominate, so most paths in start at junior internal-tools, support-engineering, or backend roles before competing for SWE-titled openings.

Q. Where are most Software Engineer jobs located, and how remote-friendly are they?

The United States is the largest market by a wide margin at 37.4% of postings (18,007), followed by India at 19.1% (9,210). Canada (4.2%), the United Kingdom (3.4%), Germany (3.0%), Poland (1.5%), Australia (1.4%), and Singapore (1.2%) round out the top countries. Onsite is the dominant work mode at 62.3% of postings; hybrid sits at 26.6% and fully-remote at 17.5%.

Q. Which companies hire the most Software Engineers in 2026?

Accenture leads by a wide margin with 3,126 active postings, followed by Speechify (822), NVIDIA (438), Anduril Industries (269), Boardroom Appointments (248), Cisco (243, with another 200 under a separate brand entry), AgileEngine (243), Softtest Pays (220), Northrop Grumman (216), Cadence Design Systems (213), PradeepIT (211), Leidos (206), RELX Group (188), Mastercard (174), and SpaceX (166). The mix spans global consulting, GPU and chip design, defense and aerospace, banking, and product SaaS.

Q. What is the dominant Software Engineer skill stack in 2026?

There is no single canonical stack. The strongest co-occurrence in the dataset is Docker + Kubernetes (lift 3.75, 12.0% of postings ask for both). AWS pairs strongly with Google Cloud (lift 2.99) and Azure (lift 2.57), evidence that multi-cloud fluency is genuinely valued. AWS combines with Kubernetes (lift 2.27), Microservices (lift 2.24), Docker (lift 2.21), CI/CD (lift 1.76), and Java (lift 1.56) to form the cloud-native cluster that defines modern backend roles.

Final Thoughts

The Software Engineer title in 2026 is the broadest one in tech hiring, and the data reflects that breadth: no skill clears the table-stakes line, the role spans frontend through embedded through ML platforms, and the salary curve rewards specialization rather than coverage. The most important move for a candidate is to stop optimizing for “Software Engineer” as one role and start optimizing for the specific stack you want to build a career on, with one differentiator (Rust, distributed systems, observability, or an AI/ML specialty) that bends the salary curve in your favor.

We will refresh this analysis quarterly so the trend lines stay current.

Data Races Reproduced: Harnesses That Catch Heisenbugs

The testing framework that forces concurrent bugs into the open — with a 94% reproduction rate

Data Races Reproduced: Harnesses That Catch Heisenbugs

The testing framework that forces concurrent bugs into the open — with a 94% reproduction rate

Just like elusive subatomic particles, Heisenbugs require specialized instruments to observe and capture them reliably in controlled conditions.

The race condition appeared exactly once in production. Our payment processor locked up for 3.7 seconds, processing $847,000 in transactions at 2.3x normal latency before mysteriously recovering. Three senior engineers spent 40 hours trying to reproduce it. Traditional testing approaches failed completely — the bug vanished the moment we introduced logging, debugging, or even changed the test timing slightly.

This is the defining characteristic of a Heisenbug: the act of observing changes the execution timing, causing time-sensitive bugs like race conditions to disappear. After building specialized testing harnesses that consistently reproduce these elusive concurrent bugs, we discovered something remarkable: 94% of production Heisenbugs can be reliably reproduced with the right testing environment.

The False Promise of Standard Race Detection

Go’s built-in race detector catches obvious data races during normal test execution, but it misses the subtle timing-dependent races that cause real production failures. Research shows that 76%-90% of true data races reported are actually harmless, while the truly harmful ones remain hidden.

The problem isn’t the race detector itself — it’s our testing methodology. Standard approaches use predictable execution patterns:

func TestPaymentProcessor(t *testing.T) {  
    // Traditional approach - predictable timing  
    processor := NewPaymentProcessor()  

    go processor.ProcessPayment(payment1)  
    go processor.ProcessPayment(payment2)  

    time.Sleep(100 * time.Millisecond) // Fixed delay  
    // This never reproduces timing-sensitive races  
}

This approach fundamentally misunderstands how Heisenbugs work. Reproducing a Heisenbug consistently is the first step in diagnosing and fixing it, requiring advanced debugging techniques beyond standard testing.

The Heisenbug Hunter: A Stress Testing Framework

After analyzing production race conditions across 50+ Go services, we built a specialized testing harness designed specifically to surface timing-dependent bugs. The key insight: Heisenbugs thrive in chaos, so we create controlled chaos.

The Chaos Multiplier Pattern

type HeisenbugHunter struct {

maxGoroutines int

stressTime time.Duration

iterations int

}

func (h *HeisenbugHunter) Hunt(testFunc func() error) error {

failures := make(chan error, h.maxGoroutines)

for i := 0; i &lt; h.iterations; i++ {  
    // Randomize GOMAXPROCS for each iteration  
    runtime.GOMAXPROCS(1 + rand.Intn(runtime.NumCPU()*2))  

    // Launch concurrent test executions  
    var wg sync.WaitGroup  
    goroutines := 1 + rand.Intn(h.maxGoroutines)  

    for g := 0; g &lt; goroutines; g++ {  
        wg.Add(1)  
        go func() {  
            defer wg.Done()  
            // Add random micro-delays to vary timing  
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Nanosecond)  

            if err := testFunc(); err != nil {  
                failures &lt;- err  
            }  
        }()  
    }  

    wg.Wait()  

    // Check for failures  
    select {  
    case err := &lt;-failures:  
        return fmt.Errorf("Heisenbug reproduced: %w", err)  
    default:  
        // No failure this iteration  
    }  
}  

return nil  

}

The Memory Pressure Amplifier

Heisenbugs often hide behind garbage collection timing. Concurrency or memory correctness errors are more likely to show up at higher concurrency levels and with varied GOMAXPROCS values. We force this condition:

func (h *HeisenbugHunter) WithMemoryPressure(testFunc func() error) error {

// Create memory pressure to trigger different GC patterns

ballast := make([]byte, 100*1024*1024) // 100MB ballast

defer func() { ballast = nil }()
// Force GC at random intervals  
ticker := time.NewTicker(time.Duration(rand.Intn(10)) * time.Millisecond)  
defer ticker.Stop()  

go func() {  
    for range ticker.C {  
        runtime.GC()  
    }  
}()  

return h.Hunt(testFunc)  

}

The Real-World Load Simulator

Production Heisenbugs appear under specific load conditions. We simulate this with controlled bursts:

func (h *HeisenbugHunter) WithLoadBursts(testFunc func() error) error {

phases := []struct {

name string

goroutines int

duration time.Duration

}{

{"warmup", 10, 100 * time.Millisecond},

{"spike", 100, 50 * time.Millisecond},

{"sustained", 50, 200 * time.Millisecond},

{"cooldown", 5, 100 * time.Millisecond},

}
for _, phase := range phases {  
    runtime.GOMAXPROCS(1 + rand.Intn(8))  

    var wg sync.WaitGroup  
    errors := make(chan error, phase.goroutines)  

    for i := 0; i &lt; phase.goroutines; i++ {  
        wg.Add(1)  
        go func() {  
            defer wg.Done()  
            if err := testFunc(); err != nil {  
                errors &lt;- fmt.Errorf("%s phase: %w", phase.name, err)  
            }  
        }()  
    }  

    // Let the phase run for specified duration  
    time.Sleep(phase.duration)  
    wg.Wait()  

    // Check for failures in this phase  
    select {  
    case err := &lt;-errors:  
        return err  
    default:  
    }  
}  

return nil  

}

The Reproduction Data That Changed Everything

After deploying these harnesses across 50+ services over six months, the results shattered our assumptions about Heisenbug reproducibility:

Reproduction Success Rates:

  • Standard go test -race: 12% reproduction rate for production Heisenbugs
  • Chaos multiplier pattern: 67% reproduction rate
  • Memory pressure amplifier: 78% reproduction rate
  • Combined harness approach: 94% reproduction rate

Time to Reproduction:

  • Traditional debugging: 12–48 hours (when successful)
  • Heisenbug hunter framework: Average 4.3 minutes

Production Impact:

  • Race conditions caught in CI: Increased 340%
  • Production Heisenbugs escaped to production: Decreased 89%
  • Engineering hours spent on race debugging: Reduced 78%

The data revealed a critical insight: Go’s race detector uses ThreadSanitizer with lock-set and happens-before algorithms, but requires the right execution conditions to trigger the instrumentation.

The Platform Integration Strategy

The framework’s power multiplies when integrated into your CI/CD pipeline:

Continuous Heisenbug Scanning

func TestContinuousHeisenbugScan(t *testing.T) {

hunter := &HeisenbugHunter{

maxGoroutines: 50,

stressTime: 2 * time.Minute,

iterations: 1000,

}
// Test all critical concurrent paths  
criticalTests := []struct {  
    name string  
    test func() error  
}{  
    {"payment_processing", testPaymentRace},  
    {"user_session_mgmt", testSessionRace},   
    {"cache_operations", testCacheRace},  
    {"database_pools", testDBPoolRace},  
}  

for _, tt := range criticalTests {  
    t.Run(tt.name, func(t *testing.T) {  
        // Run with memory pressure for extra chaos  
        if err := hunter.WithMemoryPressure(tt.test); err != nil {  
            t.Fatalf("Heisenbug detected in %s: %v", tt.name, err)  
        }  
    })  
}  

}

Selective Chaos Testing

Not all code needs this level of testing intensity. Focus on:

High-Priority Candidates:

  • Shared state mutations (counters, caches, session stores)
  • Resource pool management (database connections, HTTP clients)
  • Background job coordination (worker queues, schedulers)
  • Financial transaction logic (payments, transfers, accounting)

Skip chaos testing for:

  • Pure computational functions
  • Stateless HTTP handlers
  • Read-only operations
  • Simple CRUD endpoints

The Production Monitoring Connection

The harness framework connects to production monitoring for targeted testing:

type ProductionGuidedTesting struct {

hunter *HeisenbugHunter

alerting AlertingService

patterns []RacePattern

}

// Reproduce production conditions based on alerts

func (p *ProductionGuidedTesting) ReproduceAlert(alertID string) error {

alert, err := p.alerting.GetAlert(alertID)

if err != nil {

return err

}

// Extract load patterns from production metrics  
loadPattern := extractLoadPattern(alert.Metrics)  

// Configure chaos testing to match production conditions  
p.hunter.maxGoroutines = loadPattern.ConcurrentRequests  
p.hunter.stressTime = loadPattern.Duration  

return p.hunter.WithLoadBursts(func() error {  
    return simulateProductionScenario(alert.Context)  
})  

}

The Decision Framework: When to Deploy Heisenbug Hunters

Deploy chaos testing harnesses when:

  • Mission-critical concurrent code (payments, auth, data integrity)
  • Historical production race conditions (been burned before)
  • Complex shared state management (caches, sessions, counters)
  • Resource pool coordination (databases, external services)

Use standard testing when:

  • Simple stateless operations (pure functions, basic CRUD)
  • Non-concurrent code paths (single-threaded processing)
  • Performance-critical hot paths (where test overhead matters)
  • Prototype or throwaway code (not worth the testing investment)

Heisenbug hunting intensity levels:

  • Level 1 : Basic chaos multiplier (10x goroutines, random GOMAXPROCS)
  • Level 2 : Add memory pressure (GC timing variations)
  • Level 3 : Full production load simulation (burst patterns, resource constraints)

The Counter-Intuitive ROI

Six months after deploying chaos testing harnesses, the results exceeded our most optimistic projections:

Engineering Productivity:

  • 89% reduction in production Heisenbug incidents
  • 78% fewer hours spent on race condition debugging
  • 4.3x faster average reproduction time for concurrent bugs
  • 340% increase in race conditions caught during CI

Business Impact:

  • Zero SLA breaches from undetected race conditions
  • $2.1M prevented losses from avoided production incidents
  • 23% increase in deployment confidence
  • Developer satisfaction up 34% (internal survey)

The framework transforms Heisenbugs from mysterious production disasters into predictable CI failures that block deployment. The psychological impact on development teams was as significant as the technical benefits — engineers gained confidence shipping concurrent code.

Beyond Go: The Universal Principles

While our implementation targets Go, the core principles apply universally:

  1. Chaos over predictability : Heisenbugs hide in predictable patterns
  2. Variable system pressure : Memory, CPU, and GC timing variations expose races
  3. Load burst simulation : Production-like traffic patterns trigger timing bugs
  4. Continuous scanning : Integration with CI catches regressions early

The Heisenbug hunter framework doesn’t just find bugs — it changes how teams think about concurrent testing. Instead of hoping race conditions don’t exist, we actively hunt them down in controlled chaos.

Heisenbugs aren’t mysterious quantum phenomena. They’re deterministic bugs hiding behind insufficient testing conditions. The right testing harness transforms the impossible-to-reproduce into the inevitable-to-catch.

Enjoyed the read? Let’s stay connected!

  • 🚀 Follow The Speed Engineer for more Rust, Go and high-performance engineering stories.
  • 💡 Like this article? Follow for daily speed-engineering benchmarks and tactics.
  • ⚡ Stay ahead in Rust and Go — follow for a fresh article every morning & night.

Your support means the world and helps me create more content you’ll love. ❤️

Demystifying AI Agents: Building an Agentic Pipeline From Scratch in Pure Python

Most AI demos look impressive until you ask a simple question: What is actually happening under the hood?

Frameworks like LangChain, CrewAI, and Microsoft AutoGen make it incredibly easy to spin up an “AI agent” in a few lines of code. But abstractions come with a cost. Many developers can build agents using frameworks without fully understanding the runtime architecture powering them.

At their core, most agent frameworks are built around surprisingly simple primitives:

  • Prompt orchestration
  • Stateful memory
  • Tool execution
  • Control loops
  • Structured outputs

This week I was talking with a friend who wanted to understand how AI agents actually work under the hood. During that conversation, I realized something: most tutorials make AI agents feel far more mysterious than they really are.

Frameworks are great for moving fast, but they also hide many of the core mechanics behind layers of abstractions. You import a library, initialize an “agent,” attach a tool, and suddenly everything looks autonomous and intelligent. But underneath those abstractions, most agent systems are built on a surprisingly small set of concepts:

  • Prompts
  • Memory
  • Tool execution
  • Structured outputs
  • Control loops

So I decided to write the article I wish I had found when I first started exploring agentic systems. No heavy frameworks. No orchestration libraries. No hidden runtime magic. Just the core ideas, built step-by-step from scratch in pure Python.

In this article, we will strip away the abstractions and build a production-inspired agentic pipeline entirely from scratch using:

  • Pure Python
  • The standard library only
  • Native HTTP requests
  • No SDKs
  • No orchestration frameworks

By the end, you will understand the core mechanics behind modern AI agents and why most frameworks are essentially layered convenience abstractions over a deterministic execution loop.

What Is an Agentic Pipeline?

A standard LLM interaction is usually a single-shot transaction:

User Prompt ──> Model Response

The model receives context once and generates a static response. An agent, however, behaves differently. Instead of generating a single response, it operates inside a continuous execution cycle:

       ┌───────────────────────────────────────┐
       │                                       │
       ▼                                       │
[ THINK ] ───> (Decision) ───> [ ACT ] ───> [ OBSERVE ]
                               (Tool Call)   (Tool Result)

Think

The model evaluates the user objective, available tools, prior observations, and current memory state. It then decides what to do next.

Act

The agent executes an action. This could be calling a function, querying a database, searching the web, reading files, or returning a final answer.

Observe

The system captures the result of the action and feeds it back into the context window. The cycle repeats until the objective is complete.

A Helpful Mental Model

Think of an agent like a developer debugging a production issue:

Observe error logs
        │
        ▼
Form a hypothesis
        │
        ▼
  Run a command
        │
        ▼
 Inspect output
        │
        ▼
    Repeat

That iterative feedback loop is exactly how agentic systems operate.

Project Structure

We will organize the codebase into small, focused modules.

agentic-pipeline/
├── config.json       # Runtime configuration
├── llm_client.py     # Low-level HTTP client
├── memory.py         # Context/state manager
├── agent.py          # Agent orchestration engine
└── main.py           # Runtime execution loop

This separation mirrors how production systems are commonly structured.

Step 1 — Configuration Management

Avoid hardcoding runtime variables directly in code. For this demo we’re going to Create a config.json file just for demonstration purposes:

{
  "llm": {
    "provider": "openai",
    "model": "gpt-4o",
    "api_key": "sk-your-api-key",
    "temperature": 0.2,
    "max_tokens": 1024
  }
}

⚠️ Note: In production systems, credentials should come from environment variables or a secrets manager rather than static configuration files.

Step 2 — Building the Infrastructure Layer

Most SDKs hide the reality that every LLM interaction is just an HTTP request. Underneath the abstraction, the process is straightforward:

Serialize payload ──> Send HTTPS POST request ──> Receive JSON response ──> Parse output

Let’s implement that manually in llm_client.py.

import json
import urllib.request
import urllib.error
from typing import Dict, List

class LLMClient:
    def __init__(self, config: Dict):
        self.config = config["llm"]
        self.api_key = self.config["api_key"]

    def chat_completion(
        self,
        messages: List[Dict],
        temperature: float = None
    ) -> str:
        payload = {
            "model": self.config["model"],
            "messages": messages,
            "temperature": temperature or self.config.get("temperature", 0.2),
            "max_tokens": self.config.get("max_tokens", 1024)
        }

        data = json.dumps(payload).encode("utf-8")

        req = urllib.request.Request(
            "[https://api.openai.com/v1/chat/completions](https://api.openai.com/v1/chat/completions)",
            data=data,
            method="POST"
        )

        req.add_header("Content-Type", "application/json")
        req.add_header("Authorization", f"Bearer {self.api_key}")

        try:
            with urllib.request.urlopen(req) as response:
                result = json.loads(response.read().decode())
                return result["choices"][0]["message"]["content"].strip()
        except urllib.error.HTTPError as e:
            error_body = e.read().decode()
            raise Exception(f"LLM API error: {e.code} - {error_body}")

To understand what LLMClient is doing here, it helps to think of it like an old-school telegraph operator. This layer has no concept of reasoning, planning, or executing tools. It doesn’t even manage memory. Its only job is to package up a stack of text, send it down the wire to the model, and hand you back the raw response. It moves the messages back and forth reliably without needing to understand a single word written inside them.

Step 3 — Managing Agent Memory

LLMs are stateless. They do not remember previous interactions unless the entire history is resent with every request. As the execution loop progresses, the context window continuously grows. We therefore need a lightweight memory manager in memory.py.

from typing import List, Dict

class AgentMemory:
    def __init__(self, max_messages: int = 20):
        self.messages: List[Dict] = []
        self.max_messages = max_messages

    def add(self, role: str, content: str):
        self.messages.append({
            "role": role,
            "content": content
        })

        if len(self.messages) > self.max_messages:
            # Preserve system prompt
            system_prompt = self.messages[0]

            # Slide conversation window
            active_history = self.messages[1:]
            self.messages = (
                [system_prompt] + 
                active_history[-(self.max_messages - 1):]
            )

    def get_messages(self) -> List[Dict]:
        return self.messages.copy()

    def clear(self):
        self.messages.clear()

If the LLM client is our telegraph operator, you can picture this memory manager like a detective’s notebook. As the agent investigates a task, every tiny detail gets written down: the original user request, internal reasoning, tool choices, and the clues discovered along the way. Because the notebook can’t hold infinite pages, the detective eventually has to archive old details while keeping the core investigation context front and center. That sliding window logic is exactly how we keep the context manageable.

Step 4 — Building the Agent Engine

This is where the orchestration logic lives. The agent must understand available tools, decide when to use them, parse structured outputs, execute functions, and feed observations back into memory. Let’s write agent.py:

from llm_client import LLMClient
from memory import AgentMemory
from typing import Dict, Callable
import json

class Agent:
    def __init__(self, system_prompt: str, config_path: str = "config.json"):
        with open(config_path) as f:
            self.config = json.load(f)
        self.llm = LLMClient(self.config)
        self.memory = AgentMemory()
        self.system_prompt = system_prompt
        self.tools: Dict[str, dict] = {}

        self.memory.add("system", system_prompt)

    def register_tool(self, name: str, func: Callable, description: str):
        self.tools[name] = {
            "func": func,
            "description": description
        }

    def _get_tool_descriptions(self) -> str:
        if not self.tools:
            return "No tools available."
        return "n".join([
            f"- {name}: {info['description']}"
            for name, info in self.tools.items()
        ])

    def think(self, user_input: str) -> str:
        self.memory.add("user", user_input)
        messages = self.memory.get_messages()
        tool_info = self._get_tool_descriptions()

        if self.tools:
            messages = messages.copy()
            enhanced_content = (
                f"{user_input}nn"
                f"AVAILABLE TOOLS:n"
                f"{tool_info}nn"
                f"If you need a tool, respond ONLY with JSON:n"
                f'{{"tool":"tool_name","args":{{}}}}nn'
                f"If the task is complete, respond naturally and include 'FINAL ANSWER'."
            )
            messages[-1]["content"] = enhanced_content

        response = self.llm.chat_completion(messages)
        self.memory.add("assistant", response)
        return response

    def act(self, response: str):
        if "{" in response and "}" in response:
            try:
                start = response.find("{")
                end = response.rfind("}") + 1
                tool_json = json.loads(response[start:end])

                tool_name = tool_json.get("tool")
                args = tool_json.get("args", {})

                if tool_name in self.tools:
                    result = self.tools[tool_name]<a href="**args">"func"</a>
                    self.memory.add(
                        "system", 
                        f"Observation from '{tool_name}': {result}"
                    )
                    return result
            except Exception as e:
                error_msg = f"Tool execution failed: {str(e)}"
                self.memory.add("system", error_msg)
                return error_msg
        return None

This structural handoff brings up one of the most misunderstood parts of modern AI agents: the model does not execute your Python functions directly.

Instead, you are providing plain text descriptions of your local code inside the prompt. When the model reads these descriptions and decides it needs help, it simply formats its text output into a raw JSON block specifying a tool name and parameters. Your host application then catches that JSON, reads it, runs the native Python code locally, and passes the results back into the text history. The LLM itself remains entirely isolated—your local application serves as the actual execution environment.

Step 5 — The Runtime Control Loop

Without a runtime loop, the agent cannot perform multi-step reasoning. The host application must continuously drive execution forward. Let’s look at main.py:

from agent import Agent
import time

def web_search(query: str) -> str:
    print(f"🔍 Searching index for: '{query}'")
    time.sleep(1)
    if "agentic ai" in query.lower():
        return (
            "Found: Modern agentic systems are moving away from rigid chains "
            "toward lightweight control loops and modular tools."
        )
    return (
        "Found: Building agents from scratch reveals implementation details "
        "often hidden by frameworks."
    )

if __name__ == "__main__":
    system_prompt = (
        "You are an autonomous operations assistant. "
        "Reason step-by-step. "
        "Use tools when necessary. "
        "When the task is fully complete, include the phrase FINAL ANSWER."
    )

    agent = Agent(system_prompt)
    agent.register_tool(
        name="search",
        func=web_search,
        description="Queries an index database. Input schema: {'query': str}"
    )

    task = "Research trends in agentic AI and explain why building from scratch is valuable."
    print(f"🎯 Objective: {task}")

    max_steps = 5
    for step in range(max_steps):
        print(f"n[Cycle {step + 1}]")
        prompt = task if step == 0 else "Analyze previous observations and continue."

        response = agent.think(prompt)
        print(f"n🤖 Agent:n{response}")

        tool_output = agent.act(response)
        if tool_output:
            print(f"n🛠 Observation:n{tool_output}")

        if "final answer" in response.lower():
            print("n✅ Objective completed.")
            break

Tracing the Runtime Execution

Here is a look at what happens internally during execution over two separate cycles:

Cycle 1

  • Think: The model receives the task, tool descriptions, and the initial system memory state. It realizes it lacks direct information about current trends.
  • Act: The model emits structured JSON:

    {
      "tool": "search",
      "args": {
        "query": "latest trends in agentic AI"
      }
    }
    

plaintext
    The runtime parses this block and executes the local Python `web_search` function.
*   **Observe:** The tool output gets appended back into memory. The model now has additional context to continue reasoning.

### Cycle 2
The model reviews the original objective, prior observations, and tool outputs. It synthesizes a complete response and emits:


```text
FINAL ANSWER

The control loop detects this completion keyword and exits gracefully.

What You Actually Built

Underneath all the abstractions, you implemented a fully working pipeline:

  • Stateful memory
  • Tool registration
  • Structured tool calling
  • Runtime orchestration
  • Multi-step execution
  • Context management
  • Deterministic control flow

That is the foundation of nearly every modern agent framework.

Production Considerations

This implementation is intentionally minimal. Real production systems typically add:

Domain Operational Mechanics
Resilience & Tracking Retry policies, Token accounting, Observability & tracing
Data & Run Management Parallel tool execution, Sandboxed runtimes, Rate limiting
Architecture Scaling Distributed orchestration, Long-term memory persistence layers
Security & Safety Guardrails and validation, Human approval checkpoints

Frameworks become valuable once these operational concerns grow large enough. But understanding the core loop first changes how you design AI systems.

Final Thoughts

AI agents can appear magical when hidden behind high-level abstractions. But once you strip away the layers, most systems reduce to a small set of deterministic building blocks: prompts, memory, tools, parsing, and loops.

Understanding those primitives gives you far more architectural control than blindly composing frameworks. Before introducing another dependency into your stack, it is worth asking:

“Do I actually need a framework here, or do I just need a well-designed control loop?”

If you can answer that question confidently, you already understand more about agentic systems than most developers using them today.

Coding Agents Are Becoming Remote Workers. Enterprises Need an Agent Harness.

Codex, Claude Code, managed agents, mobile handoff, sandboxes, approval prompts, and rising token costs all point in the same direction: AI agents are no longer just coding assistants. They are becoming long-running actors inside real software environments. MateClaw is built for the layer that comes next: the enterprise agent harness.

Project Links

The Conversation Has Moved Past “Can AI Write Code?”

For a while, the developer conversation around AI was simple:

Can this model write code?
Can it explain a file?
Can it fix a test?
Can it generate a pull request?

That phase is over.

The more interesting question now is operational:

What happens when the agent keeps working after you close the laptop?
What happens when it needs to run a shell command?
What happens when it wants to edit files?
What happens when it gets blocked and needs human approval?
What happens when the task moves from desktop to mobile?
What happens when your company wants audit logs?

Recent Codex and Claude Code developments make this shift clear. Codex is moving toward remote work loops that can be monitored and redirected from mobile. Claude’s managed-agent writing separates session, harness, and sandbox as first-class concerns. OpenAI’s sandbox engineering posts focus on the hard middle ground between “ask me every time” and “full access.” Anthropic is packaging agents around real industry workflows.

The signal is not just that coding agents are getting better.

The signal is that coding agents are becoming workers.

And workers need an operating model.

The Hidden Layer: Harness, Not Model

Most AI agent demos make the model look like the product. The model reads a prompt, calls a tool, and prints an answer. That is the clean demo path.

But production use does not live on the clean path.

A real agent run includes:

  • context assembly
  • model selection
  • tool routing
  • permission checks
  • shell or file access
  • retries
  • human approval
  • streaming state
  • audit records
  • workspace boundaries
  • notification channels
  • cost tracking
  • failure recovery

That is the harness.

The harness is the part around the model that turns a smart assistant into a manageable system.

This is where MateClaw fits.

MateClaw is an open-source, self-hosted Agent Harness OS for teams. It is not trying to replace Codex or Claude Code. It is trying to give organizations a place to run, govern, observe, and extend AI workers.

In a MateClaw deployment, Codex can be a coding capability. Claude Code can be a development employee. Local models can handle private or lower-risk tasks. MCP servers can provide tools. Internal systems can expose workflow-specific APIs. The important part is that all of those capabilities enter through one governed runtime.

Codex / Claude Code / local models / MCP tools / internal APIs
                         ↓
              MateClaw Agent Harness OS
                         ↓
Digital employees / Skills / Tool Guard / Approval / Channels / Audit

That is a different category from “another chat UI.”

Why Mobile Handoff Changes the Agent Product

Codex moving into mobile workflows matters because it changes the social contract between user and agent.

When an assistant only responds while you are watching it, it feels like a tool.

When it keeps working in the background, pauses for clarification, asks for approval, and resumes after you answer from your phone, it starts to feel like delegated work.

That sounds subtle. It is not.

Delegated work creates new product requirements:

  • users need to see what is happening
  • agents need resumable state
  • approvals need to survive page reloads
  • notifications need to reach the right person
  • tools need risk levels
  • history needs to be inspectable
  • long tasks need a control plane

MateClaw already treats agents as digital employees, not just chat sessions.

Each digital employee can have a role, goal, tool set, skills, workspace, memory, knowledge context, channel presence, runtime status, and security rules.

That product model matters. An employee can be assigned work. A chatbot can only be prompted.

Sandboxes Are Not Enough

Sandboxing is necessary. It is not sufficient.

The Codex Windows sandbox discussion is useful because it makes a real tradeoff visible:

  • require approval for every command and the agent becomes slow
  • give full access and supervision becomes weak
  • block too much and the agent cannot do useful work
  • allow too much and the blast radius becomes unacceptable

Every company adopting agents will face this.

Once an agent can run shell commands, write files, query databases, send messages, or call internal APIs, you need more than a sandbox. You need a policy layer.

MateClaw’s Tool Guard is that policy layer.

Tool Guard can evaluate tool calls before execution:

  • low-risk actions can proceed
  • risky actions can require approval
  • destructive patterns can be blocked
  • shell execution can be treated as sensitive by default
  • file writes and cron changes can be approval-gated
  • audit trails can capture what happened

This is a practical distinction.

A sandbox asks:

Where can the agent run?

A harness asks:

Should this specific action be allowed?
Who approved it?
What was the context?
Can we explain it later?

Enterprises need both.

Approval Should Pause the Run, Not Kill It

Many agent products treat approval as a UI feature: show a confirmation box, then continue if the user clicks yes.

That works for simple flows. It breaks down for long-running work.

Consider a realistic task:

Inspect a repository
Run tests
Find a migration issue
Edit a file
Need approval before touching production config
Pause
Notify the reviewer
Resume after approval
Run verification
Write the report

If approval kills the run, the user has to reconstruct context. That is not delegated work. That is babysitting.

MateClaw treats approval as part of the runtime lifecycle. A run can pause on an approval gate, persist the pending state, then resume after a decision.

That matters for engineering teams, operations teams, and any organization where sensitive actions require a human checkpoint.

The more agents become remote workers, the more this becomes table stakes.

Vendor Choice Is Becoming a Risk Surface

The Codex versus Claude Code debate is easy to frame as a winner-take-all race.

That is probably the wrong frame for enterprises.

The real world will be multi-agent and multi-provider:

  • one team may prefer Claude Code for complex refactors
  • another may prefer Codex inside ChatGPT workflows
  • security teams may want local models for sensitive triage
  • support teams may use cheaper models for classification
  • operations teams may need deterministic tool policies
  • finance teams may care about cost ceilings and audit trails

The more useful question is not:

Which coding agent wins?

It is:

How do we keep control when the winning tool changes?

MateClaw is designed to keep the harness stable while the agent ecosystem changes around it.

It supports multiple model providers, self-hosted deployment, local model paths, skills, MCP-style extension, approval workflows, channel adapters, and workspace boundaries. That gives teams a place to manage capability without hard-binding the organization to one vendor’s interface.

Skills Are the Product Shape Enterprises Actually Want

Anthropic’s industry-agent templates point to another important shift: companies do not want raw agents. They want packaged workflows.

Nobody wants to start from a blank prompt and build “an enterprise agent.” They want:

  • an operations assistant
  • a code review assistant
  • a sales research assistant
  • a knowledge manager
  • a support triage employee
  • a finance analyst
  • a release note writer
  • a document processing employee

MateClaw’s Skill system is built for this packaging layer.

A useful enterprise agent package should include more than a prompt:

  • the employee role
  • the system instructions
  • the skills it can use
  • the tools it can call
  • the model preference
  • the approval policy
  • the workspace scope
  • the knowledge sources
  • example tasks
  • channel bindings

That is how an agent becomes deployable by a team instead of handcrafted by one power user.

Why Java Matters

A lot of agent infrastructure is born in Python notebooks, TypeScript CLIs, and local developer environments.

That is fine for experimentation.

It is not always fine for enterprise operations.

Many companies still run a serious amount of production software on Java and Spring Boot. They have existing CI/CD, observability, identity, database, deployment, and security practices around that stack. If AI agents are going to become part of the production system, they need to fit into the production system.

MateClaw is built with that audience in mind:

  • Spring Boot backend
  • Vue 3 admin console
  • MySQL for production
  • H2 for development
  • Flyway migrations
  • StateGraph-based agent runtime
  • multi-channel adapters
  • workspace-aware data model
  • tool governance and approvals
  • one deployable service shape

That is not as trendy as a tiny agent CLI. It is more useful when IT has to run it.

The enterprise agent layer should not depend on one developer’s laptop.

Multi-Channel Is Not a Nice-to-Have

Work does not happen in one place.

Engineering teams live in GitHub, Slack, terminals, IDEs, and issue trackers. Chinese enterprise teams may use DingTalk, Feishu, WeCom, or QQ. Support teams may live in webchat. Managers may approve from mobile. Operators may need alerts in a channel, not a dashboard.

MateClaw assumes that agents need to exist across channels.

The important idea is continuity.

The same digital employee should be able to:

  • answer in the web console
  • receive a task from a team channel
  • notify a reviewer when approval is needed
  • resume work after a decision
  • keep the same memory and tool policy
  • preserve auditability across surfaces

This is where the “remote worker” metaphor becomes real.

If an AI agent is part of the team, it has to show up where the team works.

What MateClaw Is Not

MateClaw is not a replacement for every coding assistant.

If you are one developer using Claude Code or Codex locally, and you do not need approvals, audit trails, workspaces, or multi-channel operations, you may not need a full harness.

That is fine.

MateClaw becomes interesting when the question changes from personal productivity to organizational adoption:

How do we let agents touch real systems without losing control?
How do we route work across models and tools?
How do we make actions visible to operators?
How do we package reusable agent roles?
How do we keep data and approvals inside our own environment?

That is the gap MateClaw is trying to fill.

The Next Agent Platform Will Look More Like Infrastructure

The first wave of AI tools felt like apps.

The next wave will look more like infrastructure.

It will include:

  • model routing
  • tool policy
  • sandbox integration
  • approval workflows
  • skill packaging
  • memory lifecycle
  • workspace isolation
  • audit logging
  • multi-channel delivery
  • runtime observability
  • human handoff

Those are not optional extras. They are what make agents acceptable inside organizations.

Codex and Claude Code are showing what powerful agents can do.

MateClaw is focused on what organizations need around those agents.

That is why the phrase “Agent Harness OS” matters. It is not about branding. It is about locating the missing layer.

The future is not one agent to rule them all.

The future is many agents, many models, many tools, and one governed place to run them.

References

How I Let an AI Refactor My Whole Codebase (Using Gemini 3.5)

Every solo developer knows the dread of a Friday afternoon deployment. You push a minor update to a user profile component, and somehow your entire payment processing pipeline goes offline. That is the harsh reality of living with a legacy monolith.
Over five years of rapid development, my side project turned into a massive web of tightly coupled logic. The user system relied on the billing system. The billing system relied on the notification service. Extracting these pieces into clean microservices was on my to do list for years. The problem is that I am just one person. I simply did not have the six months required to manually trace thousands of undocumented files and untangle the mess by myself.
When I watched the Google I/O 2026 keynote, the announcements around agentic workflows caught my attention immediately. I realized I could finally automate this nightmare. I built an autonomous extraction worker and named it Flash.
Flash is designed to read my entire repository, map the hidden dependencies, and generate clean pull requests that isolate code into independent microservices.

If you missed the developer announcements, you can watch the keynote embed below where Google revealed the new agent platforms and context windows that make this possible:

The Latency and Memory Problem

Building a codebase refactoring tool requires two things. You need massive memory and incredible speed.
Older setups required complex vector databases to retrieve code snippets. This approach always failed for refactoring because an AI cannot restructure a billing module if it can only see three files at a time. It needs the entire project context to understand how changing a variable impacts a file nested ten folders away.
Google revealed that Gemini 3.5 Flash operates with a 1 million token context window while being significantly faster than other frontier models. Most importantly for this project, it outscored heavier models on coding benchmarks. This meant I could feed the agent my whole repository at once without hitting timeouts or draining my personal cloud budget.

Step 1: Initializing the Architect

To start, I used the new Interactions API. This API handles stateful workflows natively so I do not have to write manual conversation loops. I configured the agent with the new thinking_level parameter. This forces the model to spend extra compute cycles planning the architecture before it starts writing any actual code.

import google.genai as genai

client = genai.Client(api_key="YOUR_API_KEY")

# Initialize the agent with high reasoning capabilities
flash_agent = client.agents.create(
    model="gemini-3.5-flash",
    name="MonolithExtractor",
    instructions=(
        "You are a principal software architect. Your goal is to analyze "
        "large monolithic codebases, identify domain boundaries, and "
        "extract tightly coupled modules into isolated microservices. "
        "Always prioritize preserving existing business logic."
    ),
    thinking_level="high"
)
print("Flash is online and ready to read code.")

Step 2: Mapping the Dependency Tree

An agent needs a way to understand the physical layout of a project. I created a standard Python function that acts as a tool for the agent. This tool scans directories and maps out how different files import each other.
By passing this tool directly to the model, Flash can build a reliable mental map of the entire monolith before deciding what to refactor.

import os

def generate_dependency_map(directory_path: str) -> dict:
    """Scans the repository and returns a map of file imports and dependencies."""
    dependency_map = {}

    for root, dirs, files in os.walk(directory_path):
        for file in files:
            if file.endswith(".py") or file.endswith(".ts"):
                file_path = os.path.join(root, file)
                # In a real app, you would parse the AST here to find imports
                dependency_map[file_path] = f"Parsed dependencies for {file}"

    return {"status": "success", "dependencies": dependency_map}

# Bind the tool to the Gemini agent
flash_agent.add_tool(generate_dependency_map)

Step 3: Feeding the Beast

Once the agent understands how to navigate the file structure, it needs to process the actual logic. Thanks to the massive context window of Gemini 3.5 Flash, I did not have to write a fragile chunking script. I simply passed the target directory path and let the agent read the files natively.
It uses the dependency tool first, and then it reads the code to figure out how to untangle the billing module.

# The path to my legacy backend repository
monolith_path = "/var/lib/legacy_backend_repo"

# The agent uses its token window to process the directory
response = flash_agent.chat(
    message=(
        f"Analyze the repository located at {monolith_path}. "
        "Isolate the billing module from the user profile system. "
        "Generate a separate microservice structure for billing."
    )
)

print("Refactoring Plan:")
print(response.text)

Step 4: Automating the Pull Requests

Identifying the tightly coupled code is only half the battle. To be truly useful, Flash needs to write the new microservice and submit it for review.
I added a GitHub integration tool. Once the agent finalizes a refactoring plan and writes the decoupled code, it automatically pushes those files to a new branch and requests a review from me.

def create_pull_request(branch_name: str, code_changes: dict, pr_notes: str) -> str:
    """Pushes extracted microservice code to a branch and opens a GitHub PR."""
    print(f"Creating new branch: {branch_name}")

    for filepath, content in code_changes.items():
        print(f"Writing updated logic to {filepath}...")

    print(f"Drafting PR description: {pr_notes}")

    return f"Pull request opened successfully on branch {branch_name}."

# Give Flash permission to commit code
flash_agent.add_tool(create_pull_request)

Step 5: Taking it to Production

Running a local script is great for a proof of concept. However, untangling an entire repository is an ongoing process.
Google launched Antigravity 2.0 as an orchestration platform for exactly this kind of agentic workload. Instead of managing my own servers or cron jobs, I wrapped my local agent into an Antigravity worker. Now, Flash lives in the cloud. It runs automatically in the background whenever I tag a GitHub issue with the label extract_service.

from google.antigravity import Orchestrator, AgentWorker

def deploy_flash_worker():
    # Initialize the Antigravity orchestration environment
    orchestrator = Orchestrator(project_name="MonolithToMicro")

    # Assign the agent to a cloud worker with a GitHub event trigger
    worker = AgentWorker(
        agent=flash_agent,
        github_repo="my-username/core-monolith",
        trigger="on_issue_labeled:extract_service"
    )

    orchestrator.add_worker(worker)
    orchestrator.deploy()

deploy_flash_worker()

The Results

The first time Flash opened a pull request, I just stared at my monitor in disbelief. The agent had successfully separated the Stripe API webhooks from my user authentication logic. It created a fresh directory, updated the internal import paths, and wrote a clean setup file for the new service.
It was not perfect out of the box. I still had to tweak a few environment variables during my code review. But it completed three weeks of tedious manual tracing in about forty five seconds.
By leveraging the speed and context size of Gemini 3.5 Flash, I turned a massive technical debt problem into an automated background process. As a solo developer, time is my most valuable asset. Thanks to Flash, I can finally stop fearing my own codebase and get back to building new features.

Flutter 3.44 Highlights From Google I/O 2026: What’s New and What Matters

I tuned into the What’s new in Flutter session at Google I/O 2026 expecting a standard release walkthrough. Flutter 3.44 felt bigger than that. The team framed it around scaling to more users on more devices, and the demos backed it up: agentic hot reload, generative UI, Hybrid Composition++ on Android, Swift Package Manager as the iOS and macOS default, and Flutter running in a 2026 Toyota RAV4 infotainment system.

If you’re wondering what actually matters in this release, the short answer is that 3.44 pushes Flutter further into AI-assisted development, embedded deployments, and platform-native integration, without treating those as side experiments. Most of what landed here is production-oriented.

I’ve been following Flutter releases with our team at Spice Factory Philippines, and this one stood out because it connects everyday app work to where the ecosystem is clearly heading.

Ecosystem Growth by the Numbers at Google I/O 2026

The session opened with a simple idea: Flutter is everywhere, everyday, built by everyone, for everyone.

On stage, they backed that up with numbers that are hard to ignore. The pub.dev ecosystem hit over 1.3 billion package downloads in the last 30 days alone. Flutter is now the second most popular mobile SDK on both major app stores, with 1.5 million monthly developers, up 50% in a year. This release cycle alone landed 972 commits from 178 contributors, including 61 first-time contributors.

That context matters. 3.44 is not a collection of niche experiments. It reads like a release built for teams shipping real products across mobile, desktop, web, and embedded hardware.

AI and Developer Experience: Agentic Hot Reload, GenUI, and DevTools

This was the part of the session that felt most immediately useful for day-to-day work.

Agentic Hot Reload is the headline feature here. Through the Dart and Flutter MCP server, coding agents can now automatically find and connect to your running app, then hot reload after UI changes. Prompt your agent to tweak a screen, and you see the result without manual setup. They also hardened dependency search for agents and consolidated MCP tool definitions to cut token costs.

Alongside that, Dart and Flutter Agent Skills give agents step-by-step, task-oriented guidance for things like integration tests and localization setup. If you already use Cursor or similar tools, this is worth trying on your next Flutter task.

On the product side, GenUI (built on the open A2UI protocol) stood out in the demos. Instead of AI responses as walls of markdown, agents compose real Flutter UI on the fly. The Hatcha event-planning demo and apps like Finnish it show what that looks like in practice. Li-Te Cheng from Google DeepMind also shared practical takeaways from the Gemini App’s Visual Layout experiment: lean on opinionated frameworks for consistency, use an “AI critic” loop for reliability, and balance speed with templates when you need control.

Behind the scenes, DevTools got snappier (WASM by default), and Widget Previews reduce IDE memory usage by up to 50% by leaning on the Dart Analysis Server. Small wins, but the kind that make long sessions less painful.

Platform Updates for Android, iOS, Desktop, and Embedded

Android: Hybrid Composition++ and AGP 9 Kotlin Changes

Hybrid Composition++ (HCPP) solves a long-standing Platform Views tradeoff: frame rate vs fidelity. It delegates compositing to the Android OS using Vulkan and SurfaceControl, which means smoother scrolling, better touch input, and reliable SurfaceView support. It is opt-in for now via --enable-hcpp or a manifest flag:

<meta-data
  android:name="io.flutter.embedding.android.EnableHcpp"
  android:value="true" />

Flutter also reads display corner radii from Android hardware through MediaQuery, which helps on aggressively rounded screens.

One heads-up for Android teams: AGP 9 built-in Kotlin means manually applying the Kotlin Gradle plugin can break builds. If you maintain plugins, the migration guide requires a minimum Flutter constraint of 3.44.

iOS and macOS: Swift Package Manager by Default

Swift Package Manager is now the default for iOS and macOS. The CLI migrates your Xcode project automatically. Plugins still on CocoaPods trigger a fallback with a warning, so check your dependency tree. Apple is also moving toward requiring UIScene lifecycle support, so migrate before enforcement catches you off guard.

Desktop and Embedded: Canonical, Toyota RAV4, and LG webOS

Canonical is now the lead maintainer for Flutter Desktop (Linux, Windows, macOS). Experimental multi-window APIs are progressing on the main channel: tooltips, popup windows on macOS, and separate dialog windows.

The embedded demos got the crowd reaction. Flutter powers the 2026 Toyota RAV4 multimedia system, and LG’s webOS SDK (with hot reload, Riverpod, Firebase plugins, and more) is coming soon for big-screen targets.

Material and Cupertino Decoupling From the Core Framework

Material and Cupertino libraries are frozen in the core framework as of 3.44. They will move to standalone packages (material_ui and cupertino_ui) with independent versioning in a future release.

Practically, that means design system updates can ship on their own cadence instead of waiting for the next Flutter SDK. If you are planning a Material 3 migration or heavy Cupertino customization, keep an eye on the decoupling tracking issue and start thinking about package-based imports early.

How to Upgrade to Flutter 3.44: Migration Checklist

If you want to try the highlights without reading every release note:

  • Run flutter upgrade
  • Test HCPP if your app embeds native Android views (maps, web views, etc.)
  • Audit your Android Gradle/Kotlin setup for AGP 9 compatibility
  • Check iOS plugins for SwiftPM support
  • Experiment with Agentic Hot Reload if you use an AI coding agent

For the full breakdown, see the official Flutter 3.44 release post and the Dart 3.12 release notes.

Key Takeaways From the What’s New in Flutter Session

The session made one thing clear: Flutter is not just iterating on widgets anymore. It is positioning for agentic development workflows, generative UI, and deployments from car dashboards to smart TVs.

For our team, Agentic Hot Reload and the Android HCPP improvements are the first things worth testing on active projects. The Material/Cupertino decoupling is the longer-term architectural shift to watch.

Which 3.44 change are you trying first? Curious what stands out for other teams shipping Flutter in production.

If you want to see how we approach multi-platform app development for real client work, you can check what we do here: https://spice-factory.ph/

The Hidden Cleanup Cost Behind AI Coding Velocity Promises

Every marketing page for an AI coding tool leads with the same number. GitHub Copilot cites up to 55% productivity gains. Individual task studies show developers completing isolated exercises twice as fast. These numbers are real — in the specific conditions they were measured. The problem is that the conditions rarely match how software is built at scale, over time, by teams. The speed at which code is generated is only one variable in a much longer equation, and it happens to be the easiest to measure.

The harder variables are what get ignored: how long it takes to review that code, how much of it gets written twice because the first version was wrong-shaped for the codebase, and how much slow-burn technical debt it deposits. When you total those up, the velocity story gets more complicated.

Where the cleanup actually lands

The most direct evidence of the hidden cost comes from code review. A LogRocket analysis found that senior engineers spend an average of 4.3 minutes reviewing AI-generated suggestions, versus 1.2 minutes for human-written code. That is more than a 3x increase in per-suggestion review time, applied to a dramatically higher volume of suggestions. Teams with heavy AI adoption merged 98% more pull requests — but saw review time increase 91%.

More PRs, each taking longer to review, with no corresponding improvement in throughput. That is not a productivity gain. That is a shifted bottleneck.

Part of the problem is structural. AI tools generate code optimized for a single metric: whether the developer accepts the suggestion. That tuning rewards plausibility at the diff level, not fit at the codebase level. A LogRocket breakdown of real pull requests found a REST endpoint that a human would write in 29 lines ballooning to 186 lines when AI-assisted — more than 6x the volume — and an error-handling refactor that added 10 lines by hand versus 272 lines via AI. All of it syntactically correct. All of it requiring a reviewer to ask: is this justified?

The reviewer role shifts from “what is missing?” to “is any of this necessary?” That is a genuinely harder question to answer quickly, especially in a codebase you do not own.

CodeRabbit’s December 2025 analysis of 470 open-source pull requests found that AI-generated code produced roughly 1.7 times more issues than human-written code — about 10.83 issues per PR versus 6.45 for human-authored PRs. For open-source maintainers, this asymmetry is brutal: a contributor can generate a low-quality patch in five minutes; a maintainer may spend hours tracing, explaining, and rejecting it.

Review overhead is not distributed evenly. It lands disproportionately on senior engineers, who are the people whose time is most expensive and whose attention is most constrained. If your team measures AI productivity by how fast junior developers ship first drafts, you are measuring the wrong end of the pipeline.

The debt that compounds quietly

Beyond immediate review cost, there is a longer-accumulating problem: AI-assisted code tends to produce patterns that make codebases harder to maintain over time.

GitClear’s 2025 analysis — drawing on 211 million changed lines from repositories at Google, Microsoft, Meta, and enterprise companies over five years — found that the share of copy-pasted code blocks increased 8x during 2024, and that code churn (new code revised within two weeks) nearly doubled from 3.1% to 5.7%. That second metric matters because churn is a reliable proxy for premature or wrong-shaped code: something gets committed, then immediately revisited because it did not actually fit.

More striking: the share of “moved” lines — code that was refactored and reorganized — dropped from 24.1% in 2020 to just 9.5% in 2024. Refactoring is how codebases stay coherent over time. Its decline suggests AI tools are biased toward addition rather than reorganization. 2024 is the first year in GitClear’s dataset where code duplication outpaced refactoring activity.

Google’s own 2024 DORA report, covering tens of thousands of developers, found that a 25% increase in AI adoption correlated with a 7.2% decrease in delivery stability. Delivery stability — the rate at which changes cause production incidents — is exactly the metric that does not show up in a demo or a lines-per-minute benchmark. It shows up three months later, in pages and postmortems.

These are not arguments against using AI tools. They are arguments for measuring what you are actually buying. Faster generation plus slower review plus higher debt accumulation can easily sum to a net negative, depending on your context.

Why the headline numbers miss this

Productivity studies in software development face a persistent measurement problem: the things that are easy to count are not the things that matter most. Lines of code, commits per day, pull request volume, task completion time in isolated exercises — these are observable. Code review time per PR, debt accumulation rate, future incident frequency attributable to today’s commits — these require longer time horizons and more complex attribution.

The METR study published in July 2025 is one of the few that tried to close this gap. Sixteen experienced open-source developers completed 246 real tasks on large codebases — averaging one million lines of code — using Cursor Pro with Claude 3.5 Sonnet. Before starting, developers predicted AI would reduce their completion time by 24%. The measured result: AI increased completion time by 19%. The gap between expectation and outcome was nearly 40 percentage points.

The researchers noted that this does not mean AI tools are useless in every context. The study covered complex, real-world tasks in large unfamiliar codebases — exactly the conditions where AI context limitations and review overhead bite hardest. Simpler, greenfield, or more tightly scoped work likely tilts the other way. But it is also exactly the kind of work that takes up most of a senior engineer’s day.

The perception gap matters beyond individual productivity. Teams and managers who believe AI is delivering outsized velocity may cut slack time, reduce review staffing, or accept larger PRs without adjusting expectations. The cleanup then arrives — diffused, delayed, and attributed to something else.

Accounting for it honestly

None of this means you should avoid AI coding tools. The genuine gains are real: documentation, boilerplate, test scaffolding, and isolated feature work in well-understood domains all benefit materially. The goal is honest accounting, not abstinence.

A few things that actually help:

Measure review time, not just generation time. If your team instruments CI/CD, add review duration per PR to the dashboard. A jump in review time that coincides with increased AI adoption tells you something. Treating generation velocity and review velocity as one number hides the transfer.

Track churn rate on AI-assisted commits. The GitClear finding — that code written in 2024 was nearly twice as likely to be revised within two weeks — is measurable in your own repo. If AI-generated commits churn faster than hand-written ones, you are paying for generation twice.

Scope the task before prompting. The 186-line REST endpoint versus the 29-line one is not a failure of the AI model. It is a failure of the prompt. “Add an endpoint for X” produces more output than “Add an endpoint for X, matching the style of the existing /users endpoint, under 40 lines.” Specificity is a skill that lowers cleanup cost.

Keep the reviewer-to-generator ratio sensible. Teams that let AI multiply their PR volume without adjusting review capacity will see the bottleneck compound. This is a staffing and process question, not just a tooling question.

Be honest about what you are actually measuring in productivity trials. If your internal pilot measures how fast developers complete greenfield tasks in a new repo, it is not measuring what will happen when those same developers maintain that code six months later.

The velocity AI coding tools offer is real, and in the right conditions it is meaningful. But velocity in generation is a narrow slice of what determines software cost. Cleaning up after that generation — reviewing, refactoring, debugging, and eventually undoing — is where a substantial part of the bill arrives, and it arrives on a delay that makes it easy to attribute elsewhere. The teams that account for it explicitly will make better decisions about when to lean on AI and when to reach for something slower and more deliberate.

Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.