The Scoped Singleton DI Bug Your AI Just Suggested

The Scoped→Singleton DI bug your AI just suggested (and how to catch it)

Of all the bugs that ship to production silently, the captured-dependency lifetime bug is one of the most expensive. It compiles. It passes your tests. It runs fine in dev. Then in production, under load, it starts corrupting data across requests. And AI assistants suggest it constantly. Here’s why — and the one Cursor rule that catches it before merge.

The bug, in 30 lines

“
You ask Cursor to add caching to OrderService. It gives you this:

`plaintext

`
// OrderService.cs
public class OrderService : IOrderService
{
private readonly IMemoryCache _cache;
private readonly OrderDbContext _db;

public OrderService(IMemoryCache cache, OrderDbContext db)
{
    _cache = cache;
    _db = db;
}

public async Task<Order?> GetAsync(int id, CancellationToken ct)
{
    if (_cache.TryGetValue(id, out Order? cached)) return cached;

    var order = await _db.Orders.FindAsync(new object[] { id }, ct);
    if (order is not null) _cache.Set(id, order, TimeSpan.FromMinutes(5));
    return order;
}

}

// Program.cs
builder.Services.AddDbContext(…);
builder.Services.AddScoped();
builder.Services.AddMemoryCache(); // ← registers IMemoryCache as Singleton

Looks correct. Compiles. Tests pass. Shipped.

What actually happens at runtime

********
IMemoryCache is registered as Singleton — one instance for the entire app’s lifetime. OrderService is registered as Scoped — one instance per HTTP request.

plaintext
On its own, that’s fine. The problem is what you cached: an Order entity, which is in turn attached to OrderDbContext — also Scoped. The cache, alive for the lifetime of the application, now holds a reference to an entity attached to a DbContext that was disposed when the original request ended.

“
Now request #2 comes in. It hits the cache, gets the order, mutates a property. Then request #3 hits the cache, sees the mutation, and decides to write something else based on it. Then request #4 wakes up the entity’s dispose-tracking and explodes with ObjectDisposedException — but only sometimes, depending on the GC pressure that day.

Welcome to the longest debugging session of your year.

Why AI assistants suggest this constantly

The patterns the AI has seen most often in its training data — short examples, blog tutorials, StackOverflow answers — almost always omit DI registration. A typical “caching with IMemoryCache” snippet looks like ten lines, with no reference to where the service is registered or with what lifetime.

The AI learned the surface pattern (“inject IMemoryCache, call .Set”) without the surrounding constraint (“…unless the consumer is Scoped and the cached value graph reaches into Scoped infrastructure”). When you ask it to add caching to your codebase, it pattern-matches against the surface form. The constraint is invisible to it.

This isn’t a “the AI is dumb” critique. Most senior developers ship this exact bug at least once. The patterns in the wild teach the wrong lesson.

The five lifetime traps to teach the AI

If you’re going to enforce one set of rules on AI-suggested .NET code, make it these:

  1. Scoped or Transient injected into Singleton

“
The classic. A Singleton constructor takes IRepository (Scoped). The Singleton captures it forever. Requests share state. Data corrupts.

The rule: when adding a constructor parameter, check the parameter type’s registered lifetime. If the consumer is Singleton and the parameter is Scoped/Transient, refuse and surface the issue.

  1. DbContext captured by anything Singleton

****
Special case of #1 but worth its own callout. DbContext is always Scoped — it has to be, it tracks per-request state. Any Singleton that captures a DbContext is a bug. If you need DB access from a Singleton, inject IServiceScopeFactory and create a scope per operation.

  1. Cached entities still attached to a DbContext

The bug from the example. The cache outlives the DbContext, but holds a graph that depends on it.

****“
The rule: what goes into long-lived caches must be either (a) AsNoTracking()’d, (b) projected to a DTO, or (c) detached explicitly.

4. HttpClient instantiated with new


A long-running app that does new HttpClient() on every call leaks sockets — eventually exhausting the connection pool. Even worse: a Singleton that captures a single HttpClient reuses DNS forever.

The rule: always inject IHttpClientFactory and call CreateClient(name). Never new HttpClient() outside of one-shot scripts.


### 
5. Hosted services touching Scoped dependencies directly


``
IHostedService is Singleton-by-construction. Inject a Scoped repo into one and it'll be alive for the lifetime of the process — every "scoped" operation will share state. Worse, the DbContext will leak.


****``````


The rule: in any BackgroundService or IHostedService, never inject Scoped dependencies directly. Inject IServiceScopeFactory and create a scope per unit of work.


## 
The Cursor rule that catches all five


``[](https://agenticstandardcontact-byte.github.io/agentic-architect/)``

````
The dotnet-di.mdc rule in Agentic Architect codifies the above. When Cursor is editing a file where DI is happening — Program.cs, Startup.cs, ServiceCollectionExtensions.cs, any class constructor — the rule activates and audits suggestions for:


- Lifetime mismatches between consumer and constructor parameters

- Captured Scoped dependencies inside hosted services or background workers

- ``

Direct HttpClient instantiation

- Captured tracked entities in long-lived caches

- Static helpers reaching into scoped infrastructure



**
The trick is the scoping: it loads only on files where DI is actually happening — not on every prompt. Your token budget stays sane. The AI stays sharp on the file you're actually in.


## 
The bigger pattern: enforce, don't suggest


********
The reframe that took me a year of using AI assistants to internalize is this: generic prompts ask the AI to suggest good patterns. Scoped rules force it to enforce them.


"Be careful with DI lifetimes" is a suggestion. The AI will agree, nod sagely, then ship the captured-Scoped bug an hour later when you're tired.


"Before suggesting any constructor change, audit the lifetime contract" is a rule. The AI now has a checklist. It pauses, runs the check, and either suggests a boundary-respecting alternative or asks you a targeted question — instead of confidently shipping the bug.


**
The first time the AI catches a Scoped-into-Singleton in code you wrote, the kit pays for itself.

---

*Originally published at [https://agenticstandardcontact-byte.github.io/agentic-architect/blog/02-scoped-singleton-di-bug.html](https://agenticstandardcontact-byte.github.io/agentic-architect/blog/02-scoped-singleton-di-bug.html). Part of the [Agentic Architect](https://agenticstandardcontact-byte.github.io/agentic-architect/) persistence kit for Cursor + .NET.*

I Built a Private AI Assistant That Queries My Git History and Project Management Data — Using Only Local LLMs

No API keys. No cloud. All data stays on my machine.

The Problem

As a web developer, I constantly need to answer questions like:

  • “Who committed the most to our main repo this month?”
  • “What files were changed for the last campaign launch?”
  • “What project tasks are still in progress for the web team?”

These answers exist — scattered across git log, project management boards, and my own memory. I was tired of digging through terminal output and clicking through boards manually.

So I built a natural language interface that lets me ask these questions in plain English and get instant answers.

The Architecture: Text-to-SQL, Not Vector RAG

Here’s the key insight that shaped the entire project:

My data is structured, not unstructured. Commits have authors, dates, and repos. Project tasks have statuses, deadlines, and assignees. This isn’t a pile of PDFs — it’s relational data that fits naturally into a SQLite database.

Traditional RAG (vector embeddings + similarity search) is built for unstructured documents. For structured data, there’s a better approach: Text-to-SQL.

User Question
    ↓
Local LLM (generates SQL)
    ↓
SQLite Database (executes query)
    ↓
Local LLM (summarizes results)
    ↓
Human-readable Answer

The LLM doesn’t store or memorize my data. It just translates my question into SQL, runs it, and explains the results.

The Data Pipeline

Step 1: Collect everything into SQLite

I wrote two Python collectors that populate a single SQLite database:

Git history collector (collect.py):

  • Runs git log across multiple repositories
  • Stores commits, file changes, branches, and tags
  • Captures author, date, message, and insertions/deletions per file

Project management collector (collect_pm.py):

  • Queries the project management platform’s GraphQL API (Monday.com in my case, but the pattern works for Jira, Linear, etc.)
  • Stores boards, items, and subitems
  • Extracts status, assignee, department, and deadline
  • Flags web-team tasks automatically (is_web = 1)

The result: a single SQLite database holding everything needed to answer cross-cutting questions.

Step 2: Link git branches to project tasks

This was the crucial step. Git branches like feature/example-promo-banner don’t obviously connect to project items like “Example Promo Banner — Launch”.

I created a branch_task_map table that links them:

SELECT branch_name, task_name, board_name
FROM branch_task_map
WHERE branch_name LIKE '%promo-banner%'

This lets the system cross-reference: “What tasks relate to this branch?” or “What commits were made for this launch?”

The RAG System

Why Ollama?

Privacy was non-negotiable. Project data, commit messages, and task details shouldn’t leave the machine. Ollama runs the LLM entirely locally — no internet needed, no data sent anywhere.

I chose qwen2.5-coder:7b as the model — it’s excellent at SQL generation and runs fast on Apple Silicon.

The smart prompt

The system prompt is where the magic happens. It includes:

  1. Full database schema — auto-introspected at startup
  2. Sample values — actual repo names, anonymized author identifiers, statuses from the database
  3. Few-shot SQL examples — teaches the model the query patterns
  4. Today’s date — so “this week” and “last month” work correctly
def build_system_prompt():
    schema = get_schema()         # Auto-introspect SQLite tables
    samples = get_sample_values() # Real values from the DB
    return f"""You are a data analyst assistant...

## Database Schema
{schema}

## Sample Values
{samples}
..."""

Auto-discovery: the secret sauce

Before the LLM even sees the question, the system extracts keywords and searches across all tables:

def discover(question):
    keywords = extract_keywords(question)
    # Search task_boards, task_items, commits, branches...
    # Return matching IDs, names, values

This means when you ask “What’s happening with the example promo banner launch?”, the system has already found:

  • The matching project board
  • Related branches: feature/example-promo-banner
  • Recent commits referencing the same keywords

The LLM gets these exact values, so it writes precise SQL instead of guessing.

Self-correcting queries

If a SQL query returns 0 results, the system automatically retries with different keyword strategies:

Attempt 1: WHERE branch = 'feature/example-promo-banner'  → 0 results
Attempt 2: WHERE message LIKE '%promo banner%'             → 12 results

This handles the reality that commits are often on parent branches, not the feature branch itself.

The Result

A CLI tool where I type questions and get answers:

$ python3 main.py "who committed the most this month?"

Developer A and Developer B lead this month
with roughly 350 commits each, followed by
Developer C with around 280 commits.
$ python3 main.py "what web tasks are pending for the next launch?"

The upcoming launch has 8 web tasks remaining:
3 in progress, 2 ready for review, 3 not started...
$ python3 main.py -v "what files changed for the example promo banner?"

-- SQL: SELECT DISTINCT fc.file_path, SUM(fc.insertions)...
-- WHERE c.message LIKE '%promo banner%'...

Several template and snippet files were modified,
concentrated in the promo banner section and a few
related shared components.

Project Structure

The entire system is 8 files, ~400 lines of code, 2 dependencies:

custom-rag/
  main.py           # CLI entry point — REPL + one-shot mode
  agent.py          # LLM conversation loop (question → SQL → answer)
  db.py             # SQLite read-only, schema introspection, auto-discovery
  prompts.py        # System prompt with schema + few-shot examples
  tools.py          # Tool definitions
  formatter.py      # Rich terminal output
  config.py         # Paths and model settings
  requirements.txt  # rich, requests (that's it)

No LangChain. No vector database. No embeddings. No cloud services.

Key Takeaways

  1. Not all RAG needs vectors. If your data is structured, Text-to-SQL is simpler and more accurate than embedding everything into a vector store.
  2. Local LLMs are production-ready. Ollama + qwen2.5-coder:7b runs fast on a MacBook and generates correct SQL reliably.
  3. Auto-discovery beats prompt engineering. Instead of hoping the LLM guesses the right table values, search the database first and feed it exact matches.
  4. Privacy and simplicity can coexist. The whole system is a few hundred lines of Python, runs offline, and handles real questions.
  5. Cross-referencing is the real value. Any single data source is easy to query manually. The power comes from connecting git history with project management data in one natural language interface.

What’s Next

  • Cron job to auto-refresh data every hour
  • Adding chat message history as a third data source
  • A simple web UI for non-terminal users
  • Open-sourcing a generic version of the tool

Built with Python, SQLite, Ollama, and qwen2.5-coder. All code runs locally — no data leaves the machine. All examples in this article use illustrative names and rounded figures; real commit authors, project codenames, and counts have been replaced or generalized.

If you’re interested in the implementation details or want to build something similar, feel free to reach out or comment below.

Multica: An Open-Source Platform for Managing AI Coding Agents Like Teammates

If you’ve been using Claude Code, Codex, or similar AI coding agents, you’ve probably felt the friction: you paste a prompt, watch the run, babysit the output, copy something into the next prompt, and repeat. It works, but it doesn’t scale — and it definitely doesn’t feel like working with a team.

Multica is an open-source project that tries to fix that. The pitch is simple: treat your AI agents the way you treat human teammates. Assign them issues. Watch them post updates. Let them report blockers. Have them compound skills over time.

What Multica Actually Does

At its core, Multica gives your coding agents a place to live inside your team’s workflow. Instead of operating a chatbot in isolation, you assign tasks to an agent the same way you’d assign a GitHub issue to a colleague. The agent picks it up, executes it on a runtime (your local machine or a cloud instance), streams progress back in real time, and posts comments when it needs clarification or hits a wall.

A few things stand out:

Task lifecycle management. Tasks move through states: enqueue, claim, start, complete, or fail. You’re not just running a command and hoping — you have visibility into where each task is.

Reusable skills. When an agent solves something well — a deployment script, a migration pattern, a code review checklist — that solution becomes a reusable skill the whole team can pull from. Skills accumulate over time, which is where the “compound” part of the tagline comes from.

Multi-agent, multi-workspace. You can have multiple agents running on different runtimes, organized into workspaces. Each workspace is isolated with its own issues, agents, and settings.

Vendor-neutral. Multica works with Claude Code, Codex, OpenCode, OpenClaw, Hermes, Gemini, Pi, and Cursor Agent. You’re not locked into one provider.

The Architecture in Plain Terms

The stack is straightforward:

┌──────────────┐     ┌──────────────┐     ┌──────────────────┐
│   Next.js    │────>│  Go Backend  │────>│   PostgreSQL     │
│   Frontend   │<────│  (Chi + WS)  │<────│   (pgvector)     │
└──────────────┘     └──────┬───────┘     └──────────────────┘
                            │
                     ┌──────┴───────┐
                     │ Agent Daemon │  runs on your machine
                     └──────────────┘
  • Frontend: Next.js 16 with the App Router
  • Backend: Go with the Chi router, sqlc for type-safe queries, and gorilla/websocket for real-time streaming
  • Database: PostgreSQL 17 with pgvector (for skill embeddings and similarity search)
  • Agent runtime: A local daemon that auto-detects whatever agent CLIs you have on your PATH

The daemon is the key piece. It bridges your machine (where the actual agent CLI lives) with the Multica server (cloud or self-hosted). When an agent is assigned a task, the server routes it to the appropriate daemon, which spawns the CLI process and streams output back via WebSocket.

Getting Up and Running

Installation is one line:

macOS / Linux:

brew install multica-ai/tap/multica
# or without Homebrew:
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash

Windows:

irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex

Then connect everything:

multica setup   # authenticate + start the daemon in one command

After that, open the web app, go to Settings → Runtimes, and you should see your machine listed. From there you create an agent (pick a provider and runtime), and you’re ready to assign tasks.

The CLI surface is minimal:

Command What it does
multica setup One-shot: configure, authenticate, start daemon
multica daemon start Start the local runtime manually
multica daemon status Check what’s running
multica issue list List your workspace issues
multica issue create Create a new issue
multica update Pull the latest version

If you want to self-host the whole thing (server included), add --with-server to the install script:

curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server
multica setup self-host

This pulls the official Docker images from GHCR. You’ll need Docker. The full self-hosting guide lives in SELF_HOSTING.md in the repo.

For Contributors

The dev setup is a single command:

make dev

It auto-detects your environment, sets up the .env file, installs dependencies, runs DB migrations, and starts all services. Prerequisites: Node.js v20+, pnpm v10.28+, Go v1.26+, and Docker.

The codebase is about 53% TypeScript and 43% Go — frontend and backend are clearly separated, which makes it easy to work on one without touching the other.

Multica vs Going Solo With an Agent CLI

Here’s an honest comparison of what Multica adds versus just running claude or codex directly:

What you gain:

  • A shared board where your whole team sees what agents are working on
  • Real-time streaming progress instead of waiting for a long CLI run to finish
  • A skills library that accumulates team knowledge rather than living in individual prompts
  • Multi-agent routing — different tasks can go to different agents on different machines
  • An audit trail: who assigned what, when, and what happened

What you take on:

  • Running a daemon process (or a full server if self-hosting)
  • A PostgreSQL database
  • The overhead of a web app and task board

If you’re a solo developer running occasional agent tasks, the CLI alone might be enough. If you’re on a team — even a small one — trying to coordinate multiple agents across projects, Multica addresses a real gap.

Is It Worth Trying?

It depends on where you are with AI agents.

If you’re still experimenting and running agents manually on single tasks, Multica is probably more infrastructure than you need right now. Start with the agent CLI directly and see what breaks.

If you’ve gotten past that point and are starting to feel the coordination pain — agents running on different machines, teammates not knowing what’s been automated, the same solutions being re-invented in different prompts — that’s exactly the gap Multica is designed to fill.

It’s early software (v0.2.x as of this writing), so expect rough edges. But the core loop — assign, execute, report, reuse — is working, and the momentum behind the project is real.

Links:

The Death of the Junior Developer

Somewhere right now, a hiring manager is looking at a requisition for a junior developer and asking a question that did not exist two years ago: why would I hire someone to do what my senior developers and their AI tools already handle?

Across the industry, a CS graduate is staring at a job board that looks nothing like the one their professors described. Entry-level postings are down 60% since 2022. The career path they were promised has a gap where the first rung used to be.

Both of them are right to be concerned. Neither of them has a good answer.

The Numbers Are Not Subtle

A Harvard study tracking 62 million workers across 285,000 U.S. firms found that companies adopting generative AI cut junior employment by 7 to 12 percent within six quarters. Senior employment stayed flat. The decline was driven by slower hiring, not layoffs. The juniors were not fired. They were never hired in the first place.

Stanford’s Digital Economy Lab found that employment for developers aged 22 to 25 dropped roughly 20% from its late-2022 peak. CS graduate unemployment sits at 6.1%. For ages 22 to 27, it is 7.4%, nearly double the national average.

Big Tech junior hiring fell from 32% of new hires in 2019 to 7% today. Salesforce halted new engineer hiring entirely. Klarna froze developer recruitment. A LeadDev survey found that 54% of engineering leaders plan to hire fewer juniors going forward. AI was the reason given every time.

The math is straightforward. If a senior developer with AI tools produces the output of a senior plus two juniors, the two juniors do not get hired. That is not ideology. It is a spreadsheet.

Camp One: Juniors Are Dead Weight

The efficiency camp does not enjoy saying this out loud, but they are thinking it. A senior engineer with Copilot, Cursor, or Claude Code ships more code in a week than a junior ships in a month. The code is better. It needs less review. It has fewer bugs. The senior already knows the architecture, the edge cases, the patterns. The AI compresses the implementation. The junior was the implementation.

Dario Amodei, the CEO of Anthropic, said it plainly: entry-level jobs are squarely in the crosshairs. He predicted AI could eliminate 50% of entry-level white-collar jobs within five years. He said leaders were sugar-coating what was coming.

Companies report 40 to 55 percent more code output per sprint after adopting AI coding tools. The velocity gains are real. The developer grind that produces them is relentless. And most of that velocity came from eliminating the work that juniors used to do: the boilerplate, the CRUD endpoints, the straightforward implementations that a senior would spec and a junior would type.

This camp will tell you that the market is correcting. That the industry over-hired juniors for years because implementation was expensive. That AI made implementation cheap. That hiring someone to do cheap work at expensive wages is not sustainable. That this is not cruel. It is math.

Camp Two: We Are Eating Our Seed Corn

The pipeline camp has one question the efficiency camp cannot answer: where do future seniors come from?

Matt Garman, the CEO of AWS, called replacing juniors with AI one of the dumbest things he has ever heard. His argument: they are your cheapest employees, your most AI-engaged learners, and if you stop hiring them now, you will have nobody who learned anything when you need senior engineers ten years from now.

Microsoft’s Mark Russinovich and Scott Hanselman published a paper in the Communications of the ACM warning that companies are optimizing for short-term throughput while the pipeline that produces future seniors quietly collapses. Their projection: a reduction in junior hiring between 2024 and 2026 means a proportional senior shortage between 2031 and 2036.

Fast Company ran a piece documenting what happens when you eliminate juniors: burnout. Senior engineers expected to absorb junior workloads plus manage AI tools are burning out faster than companies can replace them. The seniors who were supposed to benefit from not having juniors to mentor are drowning in the work juniors used to share.

This camp will tell you that senior engineers do not appear from nowhere. They are built. Through years of shipping bad code and learning from the failure. Through debugging production incidents at 2am and understanding why the shortcut they took six months ago is the reason they are awake. Through code reviews where a more experienced developer explained not just what was wrong but why it mattered.

Remove that process and you do not get a more efficient industry. You get an industry with a ten-year expiration date on its talent supply. The security consequences of that talent gap are already visible.

The Learning Paradox Nobody Talks About

There is a deeper problem underneath the hiring numbers. AI does not just replace junior work. It replaces junior learning.

Anthropic’s own research found a 17-point comprehension gap between developers who learned with constant AI assistance versus those who coded manually. Debugging skills showed the largest deterioration. The developers who used AI the most understood the least about what they built.

A randomized trial by METR found that experienced open-source developers using AI were 19% slower on average but believed they were faster. If experienced developers misjudge their own productivity with AI, imagine what happens to someone who has never worked without it.

The junior developer who never struggled through a bad database schema never learned why normalization matters. The one who never optimized a slow query by hand never developed intuition for what the database is actually doing. The one who never shipped without tests and got paged at 2am never internalized why testing is not optional.

The pain was the education. Remove the pain and you remove the education. What remains is someone who can prompt an AI to produce code they cannot evaluate, debug, or explain. That is not a junior developer. That is an operator who does not understand the machine they are operating. And when their code passes through a review process that has become a rubber stamp, there is no safety net left.

The Trust Problem

Research shows that junior developers trust AI output at 78%. Seniors trust it at 39%. That gap is not about skepticism versus enthusiasm. It is about pattern recognition.

A senior developer has seen enough production failures to know where AI tends to go wrong. They have intuitions about security, performance, and maintainability that were built through years of consequences. When the AI produces something that looks clean but has a subtle flaw, the senior catches it because they have been burned by that class of flaw before.

A junior developer does not have that library of failures. They cannot evaluate what they cannot contextualize. They accept the output because they have no basis for skepticism. The code compiles. The tests pass. The PR gets approved. The flaw ships to production and nobody knows until a customer finds it.

The paradox: AI is most dangerous in the hands of the people most likely to trust it completely. And the people most likely to trust it completely are the ones with the least experience to validate what it produces.

Neither Camp Has a Plan

The efficiency camp has no answer for the pipeline problem. You cannot run an industry on seniors forever. They retire. They burn out. They leave for management or consulting or farming or whatever people do when they are tired of being on call. If nobody replaces them, you do not have a talent shortage. You have a talent extinction.

The pipeline camp has no answer for the economics. Telling companies to hire people they do not need for work that can be automated is not a workforce strategy. It is charity with a training label. No CFO is going to approve headcount for someone whose primary value is that they might become useful in seven years.

The real answer is probably somewhere in the middle, and neither camp wants to hear it. The junior developer role as it existed from 2010 to 2022 is gone. The work that defined it, the boilerplate and the basic implementations, is automated. That specific job is not coming back.

But the need for people who are learning to become senior engineers has not gone away. The role has to change. What that looks like, whether it is apprenticeships, preceptorships like Microsoft proposed, or something entirely new, nobody has figured out yet. The industry is moving too fast to stop and think about it, which is exactly why it needs to stop and think about it.

The Question Nobody Wants to Ask

If you are a hiring manager: look at your team in five years. Where do your senior engineers come from? If the answer is other companies, ask yourself what happens when every company has the same plan.

If you are a junior developer: the career path that existed for the generation before you is gone. That does not mean there is no path. It means the path has not been built yet and you might have to help build it. The developers who figure out how to combine AI tools with genuine understanding, not just prompting but actual comprehension, will be extraordinarily valuable. The ones who use AI as a crutch instead of a tool will be the ones the efficiency camp points to when they justify not hiring the next cohort.

If you are a senior developer: the juniors you mentor today are not slowing you down. They are the reason someone will be available to maintain your systems after you move on. The thirty minutes you spend on a code review teaching a junior why their approach has a hidden performance problem is not lost productivity. It is the only investment that produces senior engineers.

The junior developer is not dead. But the junior developer role as we knew it is. And the industry has about five years to figure out what replaces it before the consequences become impossible to reverse.

System prompts are not a security boundary for AI agents

AI agents are moving from generating text to taking actions.

They can run commands, send emails, issue refunds, update records, call internal tools, and touch production workflows.

That changes the security model.

A system prompt can guide an agent, but it should not be the thing that enforces policy.

If an action has a real side effect, there should be a control point before that action happens.

The problem

When an agent calls a tool, the important event is not the text the model generated.

The important event is the tool call.

That is where something real can happen.

A model can be manipulated.
A model can hallucinate.
A user can ask for something risky.
A prompt can be ignored or misunderstood.

So the question should not only be:

Did the model intend to do this?

It should also be:

Should this action be allowed under company policy?

A simple example

Imagine a support agent that can issue refunds.

A prompt might say:

Only issue refunds when appropriate.

That is useful guidance, but it is not enforcement.

A better pattern is to check the action before the refund tool executes.

For example:

Refund under $100: allow
Refund between $100 and $500: require approval
Refund over $500: block

Now the rule is not just hidden inside the prompt.

It is enforced before the tool callback runs.

What Enforra does

I’m building Enforra as an open source SDK for AI agent runtime control.

It sits before your tool callbacks and returns a decision before anything executes:

allow
block
require_approval
log_only

The application still owns the actual tool execution.

Enforra does not run your tools remotely.

It gives your app a policy decision before the callback is called.

Why this matters

As agents move into production, teams need more than prompt instructions and logs after the fact.

They need clear policy checks around actions that matter.

That becomes important when agents can touch money, customer data, internal systems, production infrastructure, or business workflows.

The goal is not to make agents less useful.

The goal is to make sure useful agents have a clear control point before they do something risky.

Open source

The initial Enforra SDK is here:

https://github.com/enforra/enforra

NIS2 Article 21 in Azure: Implementing Network Security Controls with Terraform

NIS2 Article 21 in Azure: Implementing Network Security Controls with Terraform

Tags: terraform, azure, security, devops

Canonical URL: https://woitzik.dev/blog/nis2-article-21-azure-terraform

Most NIS2 content is written by lawyers for lawyers. This article maps Article 21 network security requirements to concrete Azure resources — with Terraform code, not legal theory.

Disclaimer: This covers technical network infrastructure controls relevant to NIS2 Article 21. Full compliance requires organizational measures beyond infrastructure code. Nothing here constitutes legal advice.

What NIS2 Article 21 Actually Requires (For Engineers)

Four concrete network security areas:

  1. Network Segmentation — isolate systems by criticality
  2. Access Control — Default-Deny at network and identity layer
  3. Minimize Attack Surface — eliminate unnecessary public endpoints
  4. Auditability — traceable, version-controlled infrastructure changes

Here’s how each maps to Azure Terraform resources.

Control 1: Network Segmentation via Hub & Spoke

Spoke-to-Spoke traffic blocked by default — all cross-workload communication routes through the Hub:

resource "azurerm_virtual_network_peering" "hub_to_spoke1" {
  name                      = "peer-hub-to-spoke1"
  virtual_network_name      = azurerm_virtual_network.hub.name
  remote_virtual_network_id = azurerm_virtual_network.spoke1.id
  allow_forwarded_traffic   = true
}

Spokes only peer with the Hub — never with each other. Cross-Spoke traffic must be explicitly routed through the Firewall.

Control 2: Default-Deny NSG Baseline

security_rule {
  name                       = "Allow-VNet-Inbound"
  priority                   = 100
  access                     = "Allow"
  source_address_prefix      = "VirtualNetwork"
  destination_address_prefix = "VirtualNetwork"
}

security_rule {
  name                       = "Deny-Internet-Inbound"
  priority                   = 4096
  access                     = "Deny"
  source_address_prefix      = "Internet"
  destination_address_prefix = "*"
}

Priority gap (100 → 4096) leaves room for application rules without restructuring the baseline.

Control 3: Egress Control via Forced Tunneling

All outbound Spoke traffic through Azure Firewall — logged, inspectable, FQDN-controlled:

route {
  name                   = "route-to-firewall"
  address_prefix         = "0.0.0.0/0"
  next_hop_type          = "VirtualAppliance"
  next_hop_in_ip_address = azurerm_firewall.fw.ip_configuration[0].private_ip_address
}

# Critical — without these, Windows VMs lose activation
route {
  name           = "bypass-azure-kms"
  address_prefix = "23.102.135.246/32"
  next_hop_type  = "Internet"
}

The Firewall log is your audit trail for NIS2 Article 21(2)(b) — monitoring and incident detection.

Control 4: Zero Public PaaS Endpoints

resource "azurerm_key_vault" "kv" {
  public_network_access_enabled = false

  network_acls {
    default_action = "Deny"
    bypass         = "AzureServices"
  }
}

An external scanner gets no TCP connection — the endpoint doesn’t resolve to a public IP. Directly satisfies NIS2 Article 21(2)(h).

Control 5: No Static Credentials

Managed Identity + scoped RBAC — no connection strings, no access keys:

resource "azurerm_role_assignment" "app_kv_secrets" {
  scope                = azurerm_key_vault.kv.id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_linux_function_app.app.identity[0].principal_id
}

When the resource is deleted, identity and permissions are automatically destroyed.

Control 6: Auditability via IaC

Every change is a Git commit. terraform plan is an audit artifact. No ClickOps, no undocumented Portal changes.

What This Covers — and What It Doesn’t

This addresses technical network controls only. Full NIS2 compliance also requires incident response procedures, supply chain risk management, business continuity measures, and organizational governance.

Infrastructure code is the foundation. Compliance is the full building.

Full article with complete code for all six controls on my blog.

👉 Full article on woitzik.dev

How I Built My Own Laravel Analytics Package (and Almost Didn’t Crash Production)

Why Not Google Analytics? (Or why I love reinventing the wheel)

To be honest, this wasn’t an easy call. I’ve been in development for quite a while and had grown accustomed to GA. What could be simpler: you slap a script with your ID onto the site, and data starts flowing into the analytics console. All that’s left is to wait for the traffic to roll in and then analyze it by country, gender, age groups, and so on.

Yes, that’s how it used to be, but in today’s reality, there are objective reasons to rethink this concept. Let’s be real: hooking up Google Analytics in 2026 is like putting a massive deadbolt on your front door but leaving the keys with your neighbor. Everything seems under control, but the neighbor knows exactly when you came home, what you bought, and why you have a long face. And when someone visits you and wants to stay incognito, the neighbor won’t give them the keys, and they won’t even tell you they stopped by. Their response would be something like: “Nobody came, I never sleep, everything is under control…”. What am I getting at?

Remember the General Data Protection Regulation (GDPR)?

Now, for your analytics to work, you must display a cookie consent banner and get the user’s permission. And that’s where the problem lies. 90% of users don’t accept all cookies—only the strictly necessary ones. And what does that mean? Google Analytics ends up dead in the water. Besides, everyone is sick and tired of these banners. So, if there’s a legal way to ditch them, why not take it?

(To learn how to avoid legal trouble with data protection laws, check out the article GDPR Without the Headache: A Guide for Web Developers in Germany )

The decision to drop GA was made. When I decided to hit the “Eject” button and catapult myself out of the Google ecosystem, I faced a logical question: what would fill the void? Because I still needed the numbers. I started looking at the heavy artillery.

The first candidate was Matomo (formerly Piwik): Probably the most powerful all-in-one machine. It’s like keeping a pet elephant in your backyard. It does everything, the database grows like crazy, but it requires a separate PHP server, MySQL, and constant babysitting. For my small pet projects, it felt like trying to drive nails with a microscope.

The second tool I looked at was Plausible / Fathom: Sleek, modern, and privacy-respecting. But there’s a catch: you either pay a subscription (a questionable investment for a free tool) or you mess around with self-hosted Docker versions, which also eat up a good chunk of your VPS RAM.

I looked at all of this and thought: “Do I really need to spin up an entire infrastructure just to know that 50 people read my article on German taxes yesterday?”. That’s when it hit me: I don’t need a “combine harvester.” I need a tiny, precision scalpel that lives right inside my Laravel application.

I wanted something of my own: lightweight, like a morning espresso, and not asking annoying questions about GDPR. Plus, I was simply curious about who all these people (and bots) were “knocking” on my security tools at oleant.net. Since this blog also needed the same tool, I decided to develop a standalone package that could be easily installed via composer and published openly on Packagist.

For those interested in diving into the code, the package is called oleant/laravel-visit-analytics, compatible with Laravel versions 10/11/12. GitHub link: https://github.com/Oleant-NET/laravel-visit-analytics

The Heart of the System: Middleware and the Magic of the Aftertaste

Which architecture to use? There are various ways to implement this. Fortunately, Laravel has a great Middleware mechanism. I decided to stick with that, but would it slow down the user? They shouldn’t have to care about my overhead. And what if the database goes down or something goes wrong? A 500 error page as the face of the site is definitely not what I was aiming for.

That’s why a crucial decision was made — to use the terminate() method. In a Laravel Middleware, it’s like a polite waiter: he brings you the check and smiles (the handle() method), and only after you’ve already left the restaurant does he go back to wipe the table and log the tip (the terminate() method).

The user has already received their page and is happy, while our server quietly and without rush writes the logs to the database in that moment. Even if something goes wrong, the client still leaves satisfied, and the waiter… just doesn’t get a tip this time. Just kidding, but here’s how it works in practice:

PHP

public function terminate(Request $request, Response $response): void
{
    try {
        // Logic to exclude non-target clients, like Admin etc.
        // …
        // Next, write our visitors to the database
        $this->logVisit($request);
    } catch (Throwable $e) {
        // If the DB takes a nap, we won't wake the user with a 500 error
        Log::error("Analytics failed, but we're keeping our cool: " . $e->getMessage());
    }
}

Lifehack: All analytics code must be wrapped in a try-catch. Believe me, there’s nothing sillier than “crashing” an entire project just because the logger didn’t have enough room for a long User-Agent or some other non-obvious case.

GDPR on the Fly: How Not to Become Public Enemy No. 1
To avoid slapping a banner the size of half the screen saying “We are watching you, bro,” I implemented IP anonymization. We simply trim the last part of the address before it ever touches the database. This anonymizes the user and fully complies with the law. Yet, we can still tell which country, data center, etc., the visit came from. We’ll talk more about data center bots once there’s more data to show.

Back to IP anonymization:

PHP

// Before: 192.168.1.154 -> After: 192.168.1.0

It’s like seeing a crowd in masks: you understand that 5 people showed up, but who among them is your neighbor — you have no clue. The law is satisfied, and so is my conscience.

Payload: Gathering Only the Goodies
Initially, I wanted to write everything that comes in the URL to the database. But then I looked at the Livewire logs, where half the state of the planet is passed in the parameters, and realized — the database would explode. So, I decided to implement filtering based on allowed parameters in the config using array_intersect_key. Now, only what I’ve personally authorized in the config ends up in the logs. Clean, orderly, and zero fluff.

The default set in the package config looks like this:

PHP

'whitelist' => [
    'utm_source',
    'utm_medium',
    'utm_campaign',
    'utm_term',
    'utm_content',
    'ref',
],

But you can, of course, change it by publishing the config in your project first:

Bash

php artisan vendor:publish --tag="visit-analytics-config"

After that, a visit-analytics.php file will appear in your config folder. There, you can not only expand the list of tracked parameters (for example, adding something like page or search) but also specify excluded paths so you don’t turn your database into a dump for admin panel or technical endpoint logs.

What’s Next?

The package started humming, and the data began to flow. I closed my laptop and went to bed, thinking I’d wake up to some visitor charts from a couple of friends. But reality turned out to be much tougher (and more interesting).

In the next episode, we’ll step through the “looking glass”: who are Palo Alto Networks, why do bots read my Terms of Service in 7 seconds, and why Linux + DuckDuckGo is a badge of quality for a visitor?

What is Microsoft E7 and Do You Need It?

If your organization is running AI agents, the answer is probably yes.

Microsoft 365 E7, also called the Frontier Suite, became available on May 1, 2026 at $99/user/month on an annual commitment. It bundles four products into one SKU: Microsoft 365 E5, Microsoft 365 Copilot, the Microsoft Entra ID Suite, and Microsoft Agent 365. Buy them separately and you’re looking at roughly $117/user/month. The bundle saves you about $18 per user, or around 15%, before any CSP promotions apply.

The first three components won’t surprise anyone who’s been watching Microsoft licensing. E5 is the existing enterprise security and productivity foundation. Copilot is the AI assistant most organizations have already been pricing as a standalone add-on since 2024. Entra ID Suite adds the deeper identity governance features – Privileged Identity Management, Identity Protection, lifecycle workflows – that E3/E5 customers typically bolt on separately.

Agent 365 is the piece most IT teams haven’t budgeted for yet. That’s the one worth paying attention to.

Why Agent 365 Changes the Equation
AI agents are already in production at most large organizations, whether IT knows about it or not. IDC projects 1.3 billion AI agents in use by 2028, and 80% of the Fortune 500 are already running Microsoft’s agentic capabilities. The problem isn’t adoption – it’s governance. Agents that spin up without proper oversight become a shadow-IT problem fast: they access data, generate outputs, and operate under policies nobody set.

Agent 365 gives each AI agent an identity in Entra, tracks what it accesses, and enforces your existing compliance and security policies on it the same way those policies apply to people. If you’re running Copilot Studio workflows, Foundry automations, or any autonomous process that touches sensitive data, this is the control plane you need. Without it, you’re essentially running agents with no audit trail.

You can buy Agent 365 as a standalone for $15/user/month. But if you’re already on E5 with Copilot, you’re spending $117/user combined – at which point E7 at $99 just makes more sense.

The Promotions
Through December 31, 2026, Microsoft is offering CSP customers introductory discounts on E7:

  • 10% off with 10+ seats on an annual term (~$89.10/user/month)
  • 15% off with 100+ seats on an annual term (~$84.15/user/month)
  • 15% off with 300+ seats on a three-year term (~$84.15/user/month)

At 100+ seats with the 15% promo, you’re paying about $84/user instead of the $117 you’d pay stitching it together yourself. For a 150-user org, that’s roughly $59,000 in annual savings compared to separate licenses.

Who Should Skip It
E7 isn’t for everyone. If you’re on Business Premium and only want Copilot, the price jump doesn’t work out. If Entra ID P1 covers your identity needs and you have no agents in production or on the roadmap, you’re paying for governance tooling you won’t use. The trap we see most often is organizations upgrading to E7 for Copilot alone, then leaving Agent 365 dormant. If that’s your plan, stay on E5 plus standalone Copilot and revisit at renewal.

But if agents are already running in your environment – or they will be in the next 12 months – E7 is the licensing structure built for it. The governance capability isn’t a nice-to-have at that point. It’s the thing that keeps autonomous AI from becoming your next compliance problem.

Find out more in this in-depth breakdown of M365 E7.

NeuroCalT+ Mental Math

A long time ago, I read a perfect book. I really like that kind of stuff. The main problem I had back then was trying to train my skills. It is so much fun to use this method to solve math problems, but it is absolutely not as fun to create those tasks for yourself. I thought a lot about how to do it and tried several apps, but they didn’t offer what I wanted. I wanted pure math—no gamification, no ads, just examples to solve.

Moreover, I really like multiplication by 5 (even numbers). It is so simple and so impressive when you show someone how you can multiply any number like

## 84848284868682 * 5

in seconds. I am glad I met people who are interested in it as well. The story of Trachtenberg is unique, and that’s a fact.

I decided to make a small app for training, and I found that the most interesting part is multiplication. Maybe I will add some other operations in the future. If you want, you can try it out. It is free, requires no registration, has no annoying ads, and so on—just pure math. I hope you like it!

https://play.google.com/store/apps/details?id=com.milvus.grema

Şipşak özelliği kapatma! İnstagram'da şipşak nasıl ve nereden kapanır?

Şipşak özelliği kapatma! Instagram’ın mesaj kutusuna entegre ettiği “Şipşak” özelliği, kullanıcıların anlık fotoğraflarını hızlı şekilde paylaşmasına imkan tanırken sosyal medya kullanıcılarının gündeminde yer aldı. İnstagram şipşak özelliği kapatma! Snapchat benzeri yapısıyla dikkat çeken sistemde paylaşımlar kısa süre içinde kaybolurken, ekran görüntüsü alınamaması da kullanıcılar arasında merak edilen başlıklar arasında yer aldı.