The End of the `shared/` Folder: Why Capability-Driven Development (CDD) Beats DDD, FDD, and MVC


Every architectural pattern makes the same fundamental promise: isolation. We adopt new folder structures and paradigms to ensure that when we change one part of a system, the rest of it doesn’t come crashing down.

Yet, whether you are building a traditional layered application in Java Spring Boot, a Domain-Driven monolith, or a Feature-Driven system, they all inevitably fall into the exact same trap: the catch-all directory.

You know the one. It’s named shared/, core/, utils/, or shared-kernel. Over time, this folder becomes a bloated, tightly coupled bottleneck that completely breaks the isolation the architecture was supposed to provide in the first place.

If we want to build systems that scale without collapsing under their own technical debt, we have to rethink how we draw boundaries. We need Capability-Driven Development (CDD).


What is Capability-Driven Development (CDD)?

Instead of organizing by technical layers (what the code is) or isolating domains that secretly rely on a central core, CDD organizes strictly by business capability (what the system does).

To do CDD correctly and avoid recreating a catch-all folder, you must adopt two unbreakable rules:

  1. “DRY by Pkg” for Shared Logic: True shared code does not belong in a peer utils/ folder. It belongs in a Platform Capability—a highly cohesive, purpose-built package consumed via strict dependency contracts (like a Cargo.toml or package.json).
  2. Fractal Isolation: CDD goes all the way down. If a capability internally routes everything through a central api/router and shares a single models/ folder, you’ve just recreated MVC in a trenchcoat. Micro-capabilities must completely own their own transport contracts, business logic, and strictly private data structures.

To see why CDD is necessary, let’s look at how the three most popular architectural styles attempt—and fail—to solve a real-world problem: an E-commerce system.


The Traps We Keep Falling Into

Imagine we are building the backend for an e-commerce platform that handles product catalogs, checkouts, and tax calculations.

1. The MVC / Layered Trap (C# & Java Spring Boot)

In classic layered architectures, we organize by technical function.

ecommerce-mvc/
├── Controllers/ (OrderController, ProductController)
├── Services/    (OrderService, ProductService)
└── Repositories/(OrderRepo, ProductRepo)

The Flaw: High horizontal coupling. To understand how checkout works, a developer has to context-switch across three different directories. If the business decides to deprecate a feature, you have to carefully surgically remove its tentacles from the controllers, services, and repositories without breaking adjacent features.

2. The DDD (Domain-Driven Design) Trap

DDD improves on MVC by grouping by bounded business contexts. But it introduces a fatal flaw when handling cross-cutting concerns.

ecommerce-ddd/
├── domain/
│   ├── order-management/
│   └── catalog/
└── shared-kernel/  <-- THE TRAP
    ├── value-objects/
    └── telemetry/

The Flaw: The shared-kernel becomes the junk drawer. Developers dump everything here—from string formatters to shared models. It creates a hidden dependency graph where a minor tweak to a shared value object inadvertently breaks three different domains.

3. The FDD (Feature-Driven Development) Trap

FDD groups by granular user actions, which sounds great until you look at how it handles shared infrastructure.

ecommerce-fdd/
├── features/
│   ├── process-checkout/
│   └── calculate-tax/
└── shared/         <-- THE TRAP
    ├── utils/
    └── api/

The Flaw: Feature autonomy is an illusion. Every feature relies on the global shared/ directory to function. It is just another variation of the catch-all problem.


The CDD Solution: Fixing the E-commerce System

Here is how that exact same E-commerce system looks when built with true Capability-Driven Development. We eliminate the shared/ folder entirely, treating foundational logic as external packages and pushing APIs down into the micro-capabilities.

ecommerce-cdd-ecosystem/
├── platform/                     <-- "DRY by Pkg" (Strict API contracts)
│   └── observability/            <-- E.g., OpenTelemetry logging/metrics
│       ├── src/lib.rs
│       └── Cargo.toml            <-- The strict contract enforcer

└── business/  
    └── order-management/
        ├── src/
        │   ├── process_checkout/ <-- Micro-capability (100% self-contained)
        │   │   ├── routes.rs     <-- It owns its own API/transport contract!
        │   │   ├── logic.rs      <-- The actual business execution
        │   │   └── types.rs      <-- Strictly private data structures
        │   │
        │   ├── calculate_tax/    <-- Micro-capability
        │   │   ├── routes.rs
        │   │   ├── logic.rs
        │   │   └── types.rs
        │   │
        │   └── main.rs           <-- Mounts routes from capabilities. Zero business logic here.
        └── Cargo.toml            <-- Explicitly imports 'observability'

Why This is Bulletproof

  1. No Utilities in Disguise: If the process_checkout capability needs to parse a specific string format that calculate_tax also needs, they both get their own duplicated parsing function inside their directory. We must stop treating DRY (Don’t Repeat Yourself) as an unbreakable law. A small amount of duplication is vastly superior to brittle, tight coupling.
  2. Zero API Bottlenecks: process_checkout completely owns its own HTTP routes. If you delete the process_checkout folder, its routes disappear automatically. You don’t have to untangle it from a central router or a shared service class.
  3. Platform as a Product: Cross-cutting concerns aren’t treated as application folders. They are platform capabilities, built as independent libraries. By utilizing a package manager configuration (like Cargo.toml), the business logic is forced to declare observability as a rigid dependency. Developers can’t accidentally create spaghetti imports.

Conclusion

Whether you are building a simple web application or a massive, distributed control plane, architecture is ultimately about managing boundaries.

If you want an architecture that can actually evolve without degrading into a big ball of mud, group your system by capability. Treat your foundational logic as a strict package. Push your API contracts down into the capabilities themselves. And above all else, kill the shared/ folder.