README > Grow
TL;DR -- (1) Split your code:
domain/for logic,adapters/for database/API/files,mainto connect them. (2) Write down decisions indocs/decisions.md. That covers 80% of the chaos. Concepts 3 (ports) and 4 (testing) are optional until you need them.
Read the concepts in one sitting. Migrate your project in one or two sessions with your AI. If you are not sure you need this, check triggers.md or stay at Stage 1.
Your project has more than ten files. Your AI is putting things in the wrong places. Database code and business logic are mixing. You forgot why you made a decision. Things are getting messy.
Four new concepts. That is all. But do them in order, not all at once.
Start here (most projects only need these two):
- Split your code -- your rules in one folder, database/API/files in another, and one file that connects them
- Write down your decisions so they are not forgotten
Add later (when the folder split alone is no longer enough):
- Make your logic swappable -- change the database without rewriting your rules
- Test your code independently from the database
Concepts 1 and 2 are straightforward. Concepts 3 and 4 are more advanced, but this guide explains them in plain language. Your AI can help you implement all of them.
Split your code into three parts:
domain/ -- your rules. The calculations, the logic, the things that make your project do what it does. This code does not know about databases, APIs, or files.
adapters/ -- the outside world. Database, HTTP, file system, UI, bot. If you swap your database from SQLite to PostgreSQL, only adapter code changes. Your rules stay the same.
main -- the glue. One file that connects the two. It creates the database connection, creates the logic, and hooks them together.
Your project now looks like this:
myproject/
├── config/ <- settings, environment defaults
│ └── settings.*
├── docs/
│ ├── todo.md
│ └── decisions.md <- NEW: why you made decisions
├── src/
│ ├── domain/ <- NEW: your rules, no external dependencies
│ │ └── models.*
│ ├── adapters/ <- NEW: database, API, files, UI
│ │ ├── db.*
│ │ ├── api.*
│ │ └── cli.*
│ └── main.* <- connects domain and adapters
├── tests/
│ ├── test_domain.*
│ └── test_adapters.*
├── .gitignore
├── AGENTS.md <- updated with architecture + decisions
└── README.md
Your language might use a different folder layout. A language-specific guide is available for Python.
The one rule: adapters may use domain code. Domain must never use adapter code. Always in this direction:
adapters/ --> domain/
domain/ --> NOBODY (no external imports)
Without a record of your decisions, you (and your AI) will revisit the same questions over and over. "Should we use JSON or YAML?" gets decided, forgotten, and debated again two weeks later. That wastes time and creates inconsistency.
Create docs/decisions.md. One file, simple format:
# Decisions
## 2026-03-26: JSON instead of YAML for configuration
**Status:** active
JSON. Simpler to parse, no indentation issues, built-in support in every language.
## 2026-03-25: SQLite instead of PostgreSQL
**Status:** active
SQLite. Single user, local data, no server needed. Revisit if we need concurrent access.
## 2026-03-24: Add caching layer
**Status:** planned
Not implemented yet. Will add when response times become a problem.
## 2026-03-20: Markdown for all docs
**Status:** replaced by 2026-03-26 decision
Was plain text. Switched to Markdown for better readability.Date, decision, status, reason. Status is one of: active (still applies), planned (decided but not yet implemented), replaced (a newer decision supersedes it), or dropped (we tried it, it did not work). When your future self (or your AI) asks "why did we do it this way?", the answer is here. Stage 3 builds on this when one file is no longer enough.
As your project grows, add a Known Risks section at the bottom of decisions.md. These are things you know about but choose not to fix right now. One line per risk: what the risk is and what you do about it. If the answer is "we accept this for now", write that. Documenting them prevents the same discussion from happening again.
## Known Risks
| Risk | Mitigation |
|---|---|
| SQLite does not handle concurrent writes well | Single worker, revisit if we need multiple |
| API rate limit could block long jobs | Exponential backoff, manual retry if needed |
| Config file is not validated on startup | Add schema validation when config grows |Some checks need to happen regularly: are the docs still accurate? Are dependencies up to date? Are there secrets in the code? Add a Routines section to your todo.md:
## Routines
- [ ] Check docs match current code (last: 2026-04-10, every: 2 weeks)
- [ ] Update dependencies, check for known vulnerabilities (last: 2026-04-01, every: month)
- [ ] Security scan: no secrets in code, input validation at boundaries (last: 2026-03-25, every: month)Each routine has a last: date and an every: frequency so you know when it is due. Start with documentation accuracy and dependency audit. Add more when you feel the need.
Suggested frequencies as a starting point:
| Routine | Frequency | Why |
|---|---|---|
| Docs accuracy check | Every 2 weeks | Docs drift fast during active development |
| Dependency audit | Monthly | CVEs are published continuously. See dependency evaluation |
| Security scan | Monthly | Catches secrets or validation gaps before they accumulate |
| AI code review | Weekly | Catches hidden errors and fake safety in AI-generated code. See ai-code-review |
| Architecture check | Monthly | Detects boundary violations early |
| Performance spot check | Quarterly | Only relevant once real data flows |
| Todo cleanup | Monthly | Move resolved items from todo.md to docs/todo_archive.md |
These are defaults, not rules. A project under heavy development might check docs weekly. A stable project might audit dependencies quarterly. Adjust to your pace.
"Read my AGENTS.md and docs/todo.md. Pick the routine with the oldest
last:date. Run that check now: read the relevant files, verify the current state, report any issues, and update thelast:date. If the Resolved section in todo.md has more than a few items, move them to docs/todo_archive.md."
This is not a new concept. It is a habit that matters as soon as your project has structure. Once you split your code into domain and adapters, hidden errors become harder to find because they can happen in either layer. An adapter that silently returns empty data looks fine to your domain code. Your domain produces wrong results and nobody knows why.
The rule: when something goes wrong, the code should stop and tell you. Not continue silently, not return empty results, not pretend everything is fine. AI-generated code often hides errors behind fallback values. If your program fails silently, that is a problem to fix, not a feature to keep.
Your AGENTS.md already has the rule ("Errors must be visible"). Now is the time to enforce it. For concrete patterns and review prompts, see ai-code-review.md.
Your code in domain/ does not know whether it runs in a web server, a CLI tool, or a test. So it should not decide where or how to log. If your logic writes directly to a log file, it becomes tied to that specific setup. Tomorrow you want to log to a cloud service instead, and suddenly you are changing code that should not have changed.
Keep logging in adapters/. Your domain code returns results or raises errors. The adapter decides what to log, where to send it, and in what format.
Ask your AI to verify both habits:
"Read my AGENTS.md and check the codebase: are all errors visible (no silent catches, no swallowed exceptions, no empty fallbacks)? Is logging only in adapters/, never in domain/?"
You can stop here. Most projects do. Concepts 1 and 2 (folder split + decisions file) solve most of the chaos. If your project feels organized and your AI puts things in the right places, you are done with Stage 2. Do not add more structure just because it exists.
The next two concepts solve a specific frustration: your AI keeps importing database code inside domain/, or you want to test your logic but cannot because it is wired to a real database. If neither of these bothers you today, come back when they do.
In Concept 1 you put your logic in domain/ and your database code in adapters/. That helps with organization. But your logic still knows it talks to a database. This concept removes that last connection.
Together with Concept 1, this completes the pattern known as Ports and Adapters (also called Hexagonal Architecture). You do not need to know these names. You need to understand the idea.
Your domain code needs data from the outside (a database, a file, an API) but it should not know how that data is fetched.
Think of it like a restaurant. The chef (your domain logic) says: "I need ingredients." The chef does not care whether the ingredients come from a local farm, a supermarket, or a warehouse. The chef just describes what is needed. The supplier (your adapter) goes and gets it.
In code, this means: you write a description of what your logic needs. This description is called a port. The port talks to the adapter. The adapter talks to the database. Your logic only knows the port, never the adapter. That way, your logic cannot talk to the database directly, and you can swap the database without changing your logic.
In practice, you create three things:
-
A file in
domain/that describes what your logic needs: "I need something that can save an invoice and find an invoice by ID." It does not say how. Just what. This is called a port. -
A file in
adapters/that actually does it: "Here is how I save and find invoices using SQL." This is the adapter. -
One line in
mainthat connects the two: "Use this specific adapter to fulfill that port."
For the concrete code syntax, see languages/python.md or domain-and-adapters.md for the full pattern with examples.
Why bother? Three reasons:
- You can swap the database without touching your logic. SQLite today, PostgreSQL tomorrow, only the adapter changes.
- You can test your logic without a real database. Just create a test adapter that stores everything in memory.
- Your AI sees clear boundaries: in
domain/, no database imports allowed.
For the full pattern with code examples, see domain-and-adapters.md. For other architecture approaches, see architecture-patterns.md.
You have split your code, documented your decisions, and made errors visible. Now add one more tool: testing. Your AI writes code that checks if your other code works correctly. If something breaks later, the tests tell you before your users do.
Your code in domain/ does not need a database or a server to run. That makes it easy to test: give it input, check the output. No setup, no cleanup, no waiting. For example, testing a discount calculation:
test "10% discount on orders over 100":
order = Order(items=[Item(price=120)])
result = apply_discount(order)
assert result.total == 108
Back to the restaurant analogy: you are testing the chef's recipe, not the delivery truck. The recipe works the same whether ingredients come from a farm or a supermarket.
What to test where:
- Domain tests: pure logic, fast, no setup. "Does the calculation return the right result?"
- Adapter tests: real external calls. "Does the database adapter actually save and load correctly?"
- Security tests: boundary checks. "Does the system reject bad input?"
Ask your AI to get you started:
"Read my AGENTS.md and look at the code in domain/ and adapters/. Create tests under tests/: unit tests for all domain logic and basic security tests (reject bad input, no secrets in responses or logs)."
For what each part does and why, see testing.md.
Before you let your AI reorganize, update your project first:
Step 1: Update your AGENTS.md. Add the architecture section so your AI knows the new rules. Here is a complete Stage 2 example:
# myproject
CLI tool that converts CSV files to JSON.
## NOT
- Not a GUI (CLI is enough for one user)
- Not a web service (local only, no server complexity)
- Not handling Excel files (out of scope, CSV only)
## Rules
- Python 3.14+
- Code and comments in English
- Never overwrite input files
- No secrets in code or version control
- Errors must be visible, never hide them silently
- New files: ask first (AI rule)
- New dependencies: ask first, explain why (AI rule)
- Prefix commits with your tool name: [claude], [cursor], [codex] (AI rule)
- When uncertain: ask, don't guess (AI rule)
## Architecture
domain/ core logic, no external imports
adapters/ database, API, files, UI
main connects domain and adapters
Rule: adapters may use domain. Domain must never use adapters.
## Structure
- Source: src/ (domain/, adapters/, main.*)
- Tests: tests/
- Docs: docs/
- Decisions: docs/decisions.mdStep 2: Create the folders. Create src/domain/, src/adapters/, docs/decisions.md, and tests/ if they do not exist yet.
Step 3: Let your AI migrate the code. The safe way to migrate is one file at a time, with a commit between each move. Why? Because if something breaks, you can revert just that one step instead of untangling a massive change.
The pattern:
- Copy the file to its new location (e.g.,
utils.pytodomain/utils.py) - At the old location, create a shim (a small forwarding file that re-exports everything from the new location). This means nothing breaks yet because all imports still work.
- Update imports across the project to point to the new location
- Remove the shim once no imports use the old path
- Commit after each step
Give your AI this prompt:
"Read my AGENTS.md to understand the project. Migrate the code in src/ into domain/ and adapters/ following the architecture rules. Move one file at a time using this pattern: (1) copy file to new location, (2) leave a re-export shim at the old location so nothing breaks, (3) update all imports to the new path, (4) remove the shim, (5) commit. Files with no external dependencies go into domain/. Everything else goes into adapters/. Create a main.* that connects them. Stop after each file and let me review before moving to the next."
Step 4: Verify the result.
"Review the project structure. Check that domain/ has no imports from adapters/ and that no adapter is directly created inside domain/. Check that all external dependencies are in adapters/. Check that main.* is the only file that connects the two. Check that no re-export shims remain. Run the tests and make sure they pass."
Two signs that you need Stage 3:
- Your AI keeps violating the import rule (domain imports adapters), and you have no way to catch it automatically.
- Multiple people work on the code and conventions start to drift.
For the full list of signs, see triggers.md. Curious why these rules exist? philosophy.md explains the thinking.
Language mixing? If your AI mixes English and your native language, see language-conventions.md.