Nowadays, whenever I have a problem, my first thought is: let me build a web app for it.

I take Latin dance classes. After class, our WhatsApp group fills up with practice videos — but they get buried in scroll, and I had no way to track which moves I’d actually practiced and which I hadn’t. I used to think “there must be an app for this.” Now I think “I’ll just build one.”

Nine commits. 414 tests. One developer. Ten AI agents. One day.

The Problem: Side Projects Deserve Real Architecture

The app is simple: upload dance practice videos, browse what’s available, and track which moves I’ve practiced. A video library with course enrollment and progress tracking.

Most developers would throw this together with a flat database, no tests, and a hardcoded auth check — “just get it working.” But I’ve seen where that leads. The second feature exposes every shortcut from the first. A flat schema breaks when two entities need relationships. A hardcoded auth check can’t handle role-based access. A monolithic handler can’t be tested in isolation.

So I built this the same way I’d build production software: event-sourced aggregates, CQRS with read model projections, strict layer boundaries enforced by import-linter, and 100% test coverage. The question was whether AI could make this level of rigor feasible for a one-person side project.

The Solution: Production Architecture at Side Project Speed

The answer is yes — with AI holding the patterns and ten specialized agents handling different concerns, I can apply full DDD rigor and still move fast.

Here’s what one day looked like:

Course Enrollment (DDD stack): A complete Enrollment aggregate with enroll() and unenroll() methods, StudentEnrolled and StudentUnenrolled domain events (frozen dataclasses), command handlers, a DynamoDB event store repository with read model projections, and FastAPI endpoints. 35 new tests covering domain logic, handlers, infrastructure, and API routes.

Two-Phase SMS Authentication: First-time login uses phone number and SMS OTP via Cognito custom Lambda triggers. Three triggers — DefineAuthChallenge, CreateAuthChallenge (generates 6-digit OTP with secrets.randbelow()), VerifyAuthChallenge. After OTP verification, the user sets a password. Subsequent logins use phone + password. Every SMS costs money. After the first login, the system stops sending them.

UI Redesign: Dark theme with Latin dance energy — hot pink for Bachata, gold for Salsa, purple for Kizomba. Glassmorphism cards, gradient buttons, decorative orbs on the login screen. Enrolled courses show a pink badge. A “Show Enrolled Only” filter on the courses page. Mobile-first, because dance students check schedules on their phones.

Infrastructure fixes: A CloudFormation circular dependency where Cognito needed the Lambda ARN and the Lambda needed Cognito environment variables. Fixed by moving the env vars from Globals to the specific function. Missing DynamoDB Global Secondary Indexes for queries by name and phone number. CORS configured to use the dynamic API Gateway URL instead of hardcoded localhost.

Why This Matters With AI

The traditional tradeoff for side projects is speed vs. quality. You either build fast with shortcuts, or you build properly and it takes forever. AI eliminates this tradeoff.

Each feature followed the same pattern: I described the domain behavior, AI generated the aggregate with events and commands, the handler, the repository with event sourcing and read model projection, the API routes, and the tests. Every layer. Every time. The architecture stays consistent because the AI follows the same patterns across every feature.

The enrollment feature — aggregate, events, commands, handlers, DynamoDB repository, API routes, 35 tests — would take a human developer a full day. With AI, it was one of several features built in the same day. The AI doesn’t get tired of writing DynamoDB projection code. It doesn’t cut corners on the sixth test file because it’s bored.

The auth flow is a good example of where AI’s breadth matters. Cognito custom challenge triggers require specific Lambda handler signatures, specific challenge response formats, and specific session attribute patterns. I’ve built these before in other projects, but remembering the exact format every time is the kind of detail work that slows humans down. AI generates the correct trigger format on the first attempt because it holds the full Cognito documentation in context.

What This Looked Like in Practice

The app runs on a serverless stack: Python 3.13 on Lambda (arm64), Next.js 16 on a separate Lambda, DynamoDB for event sourcing, Cognito for auth, S3 for video storage. Six domain aggregates: Enrollment, Identity, Catalog, Progress, Recording, and Student.

The architecture enforces strict boundaries. Import-linter prevents the domain from importing infrastructure code. The domain layer is pure Python — no framework dependencies, no AWS imports. Each aggregate uses __slots__ and frozen dataclasses for immutability. Events use past tense (StudentEnrolled), commands use imperative (EnrollInCourse).

414 tests across unit, integration, acceptance, and contract levels. 100% coverage enforced — the build fails if any line is uncovered. The test pyramid is real: fast domain unit tests at the base, API integration tests in the middle, Playwright E2E tests at the top.

The frontend uses React Query for server state, Amplify for Cognito auth integration, and shadcn/ui components styled with Tailwind. The bottom navigation bar — Home, Courses, Progress, Logout — gives it a native app feel on mobile.

All of this in nine commits, one day, one developer and ten AI agents.

How to Build This In

The key is the ten agents. Each one handles a different concern: interviewing for requirements, defining domain language, planning changes, making architecture decisions, writing code, checking quality, scanning security, and handling delivery. The developer guides. The agents execute.

This is why DDD and event sourcing aren’t a deliberate “decision” here — they’re a non-decision. When ten agents handle the boilerplate of events, commands, handlers, repositories, projections, and tests, the cost of doing it properly drops to near zero. You don’t choose event sourcing because you weighed the tradeoffs. You choose it because there’s no longer a reason not to. The AI generates the full stack for every feature, every time.

The same applies to 100% test coverage. Writing 414 tests by hand for a side project would be absurd. With AI agents writing and maintaining the tests, full coverage is the default. You’d have to actively choose to skip tests — and why would you?

Enforce boundaries with tooling, not discipline. Import-linter takes five minutes to configure. The payoff is immediate: the coder agent can’t accidentally import DynamoDB in the domain layer. When you’re moving fast with AI, tooling catches what discipline misses.

Keep the same patterns across every feature. When every aggregate follows the same structure — events, commands, handler, repository, API route, tests — the agents generate each new feature faster because the pattern is established. The sixth aggregate takes less time than the first.

The Takeaway

AI doesn’t just make side projects faster — it makes production-quality side projects feasible. Full DDD architecture, event sourcing, 100% test coverage, proper auth, polished UI — all in a day’s work for one developer with ten AI agents. The next time you have a problem, you might not need to find an app for it. You can just build one.