Skip to content

Latest commit

 

History

History
312 lines (194 loc) · 16.1 KB

File metadata and controls

312 lines (194 loc) · 16.1 KB

README > Grow

Stage 2: Grow

TL;DR -- (1) Split your code: domain/ for logic, adapters/ for database/API/files, main to connect them. (2) Write down decisions in docs/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.

When You Are Here

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.

What Changes

Four new concepts. That is all. But do them in order, not all at once.

Start here (most projects only need these two):

  1. Split your code -- your rules in one folder, database/API/files in another, and one file that connects them
  2. Write down your decisions so they are not forgotten

Add later (when the folder split alone is no longer enough):

  1. Make your logic swappable -- change the database without rewriting your rules
  2. 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.

1. Split Your Code

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)

2. Document Your Decisions

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 |

Maintenance Routines

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 the last: date. If the Resolved section in todo.md has more than a few items, move them to docs/todo_archive.md."

Keep Errors Visible

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.

Keep Logging Out of Your Logic

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.


When Folders Are Not Enough

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.

3. Make Your Logic Swappable

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:

  1. 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.

  2. A file in adapters/ that actually does it: "Here is how I save and find invoices using SQL." This is the adapter.

  3. One line in main that 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:

  1. You can swap the database without touching your logic. SQLite today, PostgreSQL tomorrow, only the adapter changes.
  2. You can test your logic without a real database. Just create a test adapter that stores everything in memory.
  3. 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.

4. Start Testing

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.

How to Migrate Your Existing Code

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.md

Step 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:

  1. Copy the file to its new location (e.g., utils.py to domain/utils.py)
  2. 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.
  3. Update imports across the project to point to the new location
  4. Remove the shim once no imports use the old path
  5. 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."

When This Setup Is No Longer Enough

Two signs that you need Stage 3:

  1. Your AI keeps violating the import rule (domain imports adapters), and you have no way to catch it automatically.
  2. 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.