Open your project right now. Look at the file tree. Does it look something like this?
models.py (5,000 lines, 50 models)
services.py (4,000 lines, 30 services)
views.py (3,000 lines, 40 endpoints)
schemas.py (2,000 lines, 60 schemas) This is layer-based organization. It's the default in most tutorials. And it's destroying your ability to work with AI.
Here's why: when you ask AI to add a scheduling endpoint, it has to parse 16,000 lines of code where only 2-5% is relevant to scheduling. It's searching for a needle in a haystack. No wonder the output is wrong half the time.
The domain-based alternative
domains/scheduling/
__init__.py (Public API — what other domains can import)
models.py (65 lines — just the scheduling table)
schemas.py (55 lines — Create, Response, filters)
service.py (100 lines — business logic lives here)
router.py (40 lines — thin routing layer)
events.py (domain events)
exceptions.py (domain-specific errors)
tests/
test_service.py
test_router.py ~260 lines. 100% relevant. AI holds the entire domain in working memory and generates code that fits like it was written by someone on your team.
The numbers don't lie
| Metric | Layer-based | Domain-based |
|---|---|---|
| Context for AI | ~16,000 lines | ~1,000 lines |
| Relevant signal | 2-5% | 95-100% |
| AI first-try success | ~40% | ~88% |
| Time to fix AI output | 20-30 min | 2-5 min |
| Merge conflicts per week | 6+ | 0.4 |
The rules that make it work
- Service layer owns business logic. Endpoints are a thin translation layer — they receive HTTP, call the service, return a response. No business logic in routes, ever.
- No direct imports between domains. If scheduling needs to know about users, it communicates through events or a shared interface — never by importing
domains.users.modelsdirectly. - Public API defined in __init__.py. This is your domain's contract with the outside world. Only expose what other domains actually need.
- Events carry IDs, not objects.
UserDeactivatedEvent(user_id=uuid), notUserDeactivatedEvent(user=full_user_object). This keeps coupling minimal.
How to migrate an existing codebase
Don't reorganize everything at once. Pick your smallest, most isolated feature. Move it into a domain directory. Get it working. Then move the next one.
- Identify the feature with the fewest cross-dependencies
- Create the domain directory with the standard structure
- Move models, schemas, services, and routes into the domain
- Update imports across the codebase
- Run tests — everything should still pass
- Repeat with the next feature
Most teams can reorganize one domain per day. Within two weeks, your project structure is fundamentally different — and AI becomes dramatically more useful.