Skip to main content

Functional Packages Are a Natural Fit for AI Agents

·633 words·

For years, I’ve seen many projects default to a technical package layout.

    controller/
    ├── …
    service/
    ├── …
    repository/
    └── …

I get why, many schools teach it this way. It sort of makes sense. They often use a “layered” or “3-tiered” approach, which is a reasonable constraint to help students structure their code. It even seems to align with one of the mantras of software development: “Code that changes together, stays together.” The real problem isn’t layering itself.

The bigger issue is, we almost never change only one technical layer.

  • “Users in our system also need to have a Date of Birth.”
  • “Our sales need to keep track of their Average Revenue Per Account (ARPA)”

All these changes need to cut through all the technical layers. That’s like cutting through an onion for me… it hurts my eyes.

Instead, I have grown to love the idea of functional packages.

    sales/
    ├── …
    admin/
    ├── …
    user/
    └── …

This approach comes with many benefits.

  • Reduced cognitive load: no jumping around different directories to find all the places where I need to make a change.
  • Testing becomes easier: you test full functional scopes instead of isolated technical layers with fewer (or no) mocks and more realistic integration tests
  • Clearer ownership and encapsulation: working with a large team? You’re less in each other’s way when you work on different functional areas.

I do want to note that this is not a magical solution and comes with its own tradeoffs. Cross-cutting concerns (logging, auth, …) are harder to get right and shared domain concepts which don’t belong to a single package become a point of discussion.

Old idea, new groove
#

Functional packaging isn’t new, but using AI coding agents pushed this idea from “nice architectural preference” into something much more practical for me.

Agents work best when their context is small and coherent. The less of the codebase they need to scan to understand a change, the better their output tends to be. With functional packages, a single directory often contains everything related to one capability, which makes it a natural unit of work for an agent.

Picture of someone holding a drawing in front of their face. The drawing is of a sad face.
Generated By ChatGPT

In practice, I usually give an agent access to only one functional package at a time. This dramatically improves the quality of the generated code and makes reviews easier. It also allows me to run multiple agents in parallel, each working on a different feature without stepping on each other’s toes.

While you can ask an agent to (pretty please) only modify certain directories, I’ve seen too many posts online where an agent went rogue and ignored that request (no matter how nicely you ask).

Can I just say… what a time we live in where non-deterministic things running on our computers are normal.

Instead, I prefer to enforce boundaries at the filesystem level. I sandbox agents so they only see (and can only modify) the functional area they’re responsible for.

 # Start a Claude Code session in a docker container, with the current working directory as its workspace.
 docker sandbox run claude

Sandboxing is a good idea regardless of how your project is structured, but it pairs especially well with functional packages. The agent can’t accidentally change unrelated features because it simply doesn’t have access to them.

This isn’t foolproof. Sometimes an agent really does need broader context. But even then, sandboxing limits the blast radius and keeps agents from “helpfully” refactoring half the system.

Functional packaging won’t magically fix bad design on its own but in a world where AI agents are part of our daily workflow, it’s starting to feels like a stylistic choice and more like a practical advantage.

(Written by me, edited slightly with ChatGPT. All onion cutting tears and other pains that inspired this post were mine.)