Clean Architecture skill for Codex
The dependency rule, minus the ceremony. Codex treats Clean Architecture as one property, business policy that does not depend on web, database or SDK details, and sizes the layers to the project instead of copying a four-ring template. It removes single-implementation interfaces and mapper chains that add no value, organises code feature-first, and adds an import-linter, dependency-cruiser or ArchUnit check so the rule is enforced in CI.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- clean-architecture Installs to ~/.agents/skills/clean-architecture/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install clean-architecture The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project clean-architecture Installs to .agents/skills/clean-architecture/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/clean-architecture.zipunzip -o clean-architecture.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) clean-architecture PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $clean-architecture, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Applies Clean Architecture's dependency rule pragmatically - domain and use cases independent of frameworks, databases and UI, with layering enforced by import-linter, dependency-cruiser or ArchUnit - while sizing the number of layers, interfaces and mappers to the project instead of copying a template. Use when structuring a new service, untangling a codebase where controllers talk SQL, reviewing a layered design for over-engineering, or adding architecture tests that keep layers honest.
Use it when
- Starting a service that will hold real business rules for years
- Business logic is mixed into controllers, ORM models or framework hooks
- A layered codebase feels slow to change because of interfaces and mappers everywhere
- You want CI to block imports that break the layering
Use something else when
- The focus is isolating integrations behind ports with fakes and contract tests: use hexagonal-architecture
- You are drawing boundaries between modules of one deployable: use modular-monolith
How to check the result
A throwaway framework import in a domain file makes the CI import check fail, and domain plus use case tests pass with no database or network available.
The mistake it prevents
Copying a full four-layer template with an interface per class and DTO mappers at every boundary onto a small CRUD service, tripling the cost of every change.
The goal is one property: business policy does not depend on delivery or storage details, so it can be tested and changed on its own. Done well, that property is enforced by a check in CI, and the codebase has no layer, interface or mapper that does not pay for itself. Four concentric rings drawn on a slide do not count.
The dependency rule
frameworks & drivers (web, DB, queues, SDKs)
-> interface adapters (controllers, presenters, repositories, gateways)
-> use cases (application services, orchestration, transactions)
-> entities (domain rules, value objects)
Source code dependencies point only inward. Inner code never names an outer type, a framework annotation, an HTTP status or a table. Data crossing a boundary is in the form the inner layer defines.
Size the architecture to the project
| Project | Layers | Notes |
|---|---|---|
| Script, prototype, < 2k lines | None | Functions and modules; revisit if it survives |
| CRUD API, logic mostly validation | 2: handlers + data access, domain types shared | Framework-idiomatic (Rails, Django, Laravel conventions win) |
| Service with real business rules and 1-3 integrations | 3: domain, application, adapters | The common sweet spot |
| Large, long-lived core domain, many integrations, several delivery channels | 4 rings, feature-sliced | Use DDD for the domain ($domain-driven-design) |
Default to three layers. Add the fourth only when you can name the use cases that would otherwise be duplicated across delivery channels.
Over-engineering smells (remove these)
| Smell | Why it hurts | Instead |
|---|---|---|
IUserService with one UserService, no test double | Two files per change, no seam used | Concrete class; extract an interface when a second implementation or a fake is needed |
| Request DTO -> command -> domain -> entity -> response DTO for a field rename | Five mappers per field | Map at the outer boundary only; let use cases accept simple input types |
| One use case class per CRUD verb on every table | Hundreds of trivial classes | Use cases only where there is orchestration or policy; plain repository calls otherwise |
| Domain objects that are bags of getters/setters with logic in “services” | Layers without the benefit | Put rules in entities/value objects |
Generic Repository<T> with arbitrary filtering | Persistence leaks inward | Repository methods named for domain needs |
| Separate projects/packages per layer in a 3k-line service | Build friction | Folders plus an import check |
Recommended layout (3 layers, feature-first)
src/
billing/
domain/ # entities, value objects, domain errors - pure code
application/ # use cases, ports (interfaces for repos/gateways), transactions
adapters/ # http handlers, db repositories, external API clients
shipping/
...
main.py # composition root: build adapters, inject into use cases
Feature-first keeps a change to billing inside one folder. Layer-first at the top level
(controllers/, services/, repositories/) scatters every feature across the tree.
Workflow for an existing codebase
- [ ] 1. Read AGENTS.md; map the current layers and where business rules live
- [ ] 2. List dependency violations (inner code importing framework/DB/HTTP)
- [ ] 3. Agree the target layer count with the user (table above)
- [ ] 4. Add the import check in "report" mode and record the baseline violations
- [ ] 5. Fix violations feature by feature, behind characterisation tests
- [ ] 6. Turn the check to "fail" for fixed features; prevent new violations everywhere
Find violations quickly:
rg -n "^(from|import) (fastapi|flask|django|sqlalchemy|requests)" src/*/domain src/*/application
rg -n "from ['\"](express|pg|@prisma/client|axios)['\"]" src/*/domain src/*/application
rg -n "import (org\.springframework|jakarta\.persistence)" src/main/java/**/domain
Enforce the rule
Python: import-linter
# .importlinter
[importlinter]
root_package = app
include_external_packages = True
[importlinter:contract:billing-layers]
name = Billing layers point inward
type = layers
layers =
app.billing.adapters
app.billing.application
app.billing.domain
[importlinter:contract:domain-is-pure]
name = Domain imports no frameworks
type = forbidden
source_modules =
app.billing.domain
forbidden_modules =
sqlalchemy
fastapi
requests
Run with lint-imports (install import-linter; needs network for the first install).
include_external_packages = True is what lets a contract forbid third-party modules.
TypeScript: dependency-cruiser
// .dependency-cruiser.cjs
module.exports = {
forbidden: [
{
name: "domain-is-pure",
severity: "error",
from: { path: "^src/[^/]+/domain/" },
to: { path: "^src/[^/]+/(application|adapters)/|node_modules/(express|pg|@prisma|axios)" },
},
{
name: "application-no-adapters",
severity: "error",
from: { path: "^src/[^/]+/application/" },
to: { path: "^src/[^/]+/adapters/" },
},
],
};
Run with npx depcruise src --config .dependency-cruiser.cjs.
Java/Kotlin: ArchUnit
@AnalyzeClasses(packages = "com.acme")
class ArchitectureTest {
@ArchTest
static final ArchRule domainIsPure = noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"..application..", "..adapters..", "org.springframework..", "jakarta.persistence..");
@ArchTest
static final ArchRule applicationIgnoresAdapters = noClasses().that().resideInAPackage("..application..")
.should().dependOnClassesThat().resideInAPackage("..adapters..");
}
For Go, keep the domain in its own package with no imports of adapter packages and check
with go list -deps ./internal/billing/domain | rg "adapters|database/sql" in CI.
Boundaries and data
- Use cases accept plain input (a struct, dataclass or primitives) and return a result type or raise domain errors. They never receive an HTTP request object or return a response object.
- Transactions belong to the use case (or a unit-of-work port it calls), not to controllers and not to repositories individually.
- Validation splits in two: shape validation (types, required fields) at the adapter with the framework’s tools; business validation (credit limit, state transitions) in the domain.
- Errors: domain errors are domain types; adapters map them to HTTP status codes or exit codes.
- Read-heavy screens may query the database directly through a query service in the adapter layer. Forcing reports through aggregates is a common performance trap.
Gotchas
- Framework annotations on entities (
@Entity, Pydantic models as domain objects,@Column) are a dependency on the outer ring. Accept this knowingly for simple models and record the trade-off; do not pretend the rule holds. - “Shared” or “common” packages imported by every layer become a back door. Keep shared code to true primitives (Money, Result) with no I/O.
- Dependency injection containers are a wiring tool for the outer ring. If use cases resolve dependencies from a container, the container has become a dependency.
- Splitting into many build modules early makes refactors slow. Start with folders and an import check; promote to modules when the team or build demands it.
Verification
- The import check runs in CI and fails when you add
import sqlalchemy(or equivalent) to a domain file. Prove it once with a throwaway change, then revert it. - Domain and use case tests run without a database or network.
- Pick one recent feature: count the files touched for a one-field change. More than four or five layers of mapping is a sign to collapse layers.
Output contract
**Chosen shape:** 3 layers, feature-first (reason: <...>)
**Violations found:** <n> (baseline file: <path>), fixed: <n>
**Enforcement:** <tool>, config <path>, CI step <name>
**Simplifications made:** <interfaces/mappers removed and why>
**Deferred:** <features still violating, with plan> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Clean Architecture"
short_description: "The dependency rule, minus the ceremony"
default_prompt: "$clean-architecture Our controllers run SQL directly - restructure this service into clean layers without going overboard" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.