Design That Fits the Codebase
Traditional design happens in Miro, disconnected from the actual code. AI-assisted design happens with Gemini reading your real files — and producing diagrams, schemas, and API contracts that match what's already there. We're designing the Recurring Tasks feature for TaskFlow, picking up from the JSON spec we generated in M05.
- Generate an ER diagram showing how
RecurringTaskfits into TaskFlow's existing tables - Draft an OpenAPI 3.1 spec for the three new recurrence endpoints
- Design the
RecurringTaskSQLAlchemy model consistent with models.py - Write an Architecture Decision Record for cron vs. interval scheduling
- Generate a C4 architecture model of the complete TaskFlow system
Design With Full Context
There's a fundamental difference between "AI suggests a design" and "AI designs something that actually fits your codebase." The difference is context.
BEFORE: You hire an architect to add a room to your house. A generic architect draws plans that look great on paper — but call for a load-bearing wall where your plumbing runs.
PAIN: The builder discovers the conflict on day one. Now you're paying to redesign mid-construction. The "generic" plans cost more than a site survey would have.
MAPPING: Gemini CLI is the architect who walked the site first. When it reads models.py before designing RecurringTask, it sees that TaskFlow uses server_default=func.now() for timestamps, ForeignKey with cascade, and relationship() with back_populates. The generated design matches those patterns exactly.
Design the RecurringTask Model
Feed the actual models.py and the JSON spec from M05 together:
gemini "@models.py @database.py @spec/recurring-tasks.json
Design a RecurringTask SQLAlchemy model that fits the existing patterns
in models.py. Requirements from the JSON spec:
- Fields: task_id (FK), schedule_type, interval_value, next_run_at, is_active, created_at
- Relationship: one-to-one with Task (each task has at most one recurrence)
Match these patterns from models.py:
- Use Column(DateTime(timezone=True), server_default=func.now()) for timestamps
- Use relationship() with back_populates
- Follow the same import structure
- Add the relationship to the existing Task class too
Output only the Python code for the new model plus the modification
to the Task class. No explanations."
@models.py, @database.py, and the spec simultaneously. Gemini sees the existing import pattern (from database import Base), the timestamp convention (server_default=func.now()), and the relationship style.WHY: Without this context, Gemini might generate a model using
datetime.utcnow (deprecated in Python 3.12) or skip back_populates entirely — both are common AI mistakes when context is missing.GOTCHA: The "Output only Python code" instruction prevents Gemini from wrapping the output in explanation. For design artifacts you'll save to files, this keeps output clean.
Generated Model Code
# ── Add to models.py ──────────────────────────────────────────
class RecurringTask(Base):
__tablename__ = "recurring_tasks"
id = Column(Integer, primary_key=True, index=True)
task_id = Column(Integer, ForeignKey("tasks.id"), unique=True, nullable=False)
schedule_type = Column(String, nullable=False) # daily | weekly | monthly
interval_value = Column(Integer, nullable=True) # e.g. every N days
next_run_at = Column(DateTime(timezone=True), nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
task = relationship("Task", back_populates="recurrence")
# ── Modification to existing Task class ──────────────────────
# Add this line inside class Task, after the 'tags' relationship:
recurrence = relationship(
"RecurringTask",
back_populates="task",
uselist=False, # one-to-one: a task has at most one recurrence
cascade="all, delete-orphan",
)
server_default=func.now() exactly (not default=datetime.utcnow). It used back_populates consistently with the existing Task.owner and Task.tags relationships. It added cascade="all, delete-orphan" matching the User.tasks cascade. This is codebase-aware generation, not generic output.Mermaid Diagram Generation
A Mermaid diagramA text-based diagramming syntax that renders as visual diagrams. Supported natively in GitHub markdown, Notion, GitLab, and VS Code. You write diagram logic as text and it renders automatically — no Miro or Figma required. is text that renders as a visual. Because it's text, Gemini can generate it. Because it's renderable, it communicates to your team. Let's generate two: an ER diagram showing the new table, and a sequence diagram of the task completion flow.
ER Diagram for RecurringTask
gemini "@models.py Generate a Mermaid erDiagram showing all tables in
TaskFlow including the new RecurringTask table.
Show: primary keys, foreign keys, field types, and relationships.
Use Mermaid erDiagram syntax. Output only the Mermaid code block." \
> docs/er-diagram.md
Generated ER Diagram
```mermaid
erDiagram
users {
int id PK
string email
string username
string hashed_password
bool is_active
datetime created_at
}
tasks {
int id PK
string title
string description
bool completed
string priority
datetime due_date
datetime created_at
datetime updated_at
int owner_id FK
}
tags {
int id PK
string name
string color
}
task_tags {
int task_id FK
int tag_id FK
}
recurring_tasks {
int id PK
int task_id FK
string schedule_type
int interval_value
datetime next_run_at
bool is_active
datetime created_at
}
users ||--o{ tasks : "owns"
tasks }o--o{ tags : "has"
task_tags }o--|| tasks : ""
task_tags }o--|| tags : ""
tasks ||--o| recurring_tasks : "has recurrence"
```
||--o| notation means "one-to-zero-or-one" — a task has at most one recurrence. Gemini inferred this from the unique=True on task_id.Sequence Diagram: Task Completion Flow
gemini "@routers/tasks.py @auth.py @models.py
Generate a Mermaid sequence diagram showing the complete flow for:
PATCH /tasks/{id} where completed=True on a task with an active recurrence.
Actors: Client, FastAPI, auth.py, tasks.py router, Database, Scheduler
Show: JWT validation, task update, recurrence check, spawn logic.
Use Mermaid sequenceDiagram syntax."
auth.py to see that get_current_user uses oauth2_scheme and jwt.decode — so the diagram shows the real JWT validation flow, not a generic "check auth" box. The diagram is grounded in the actual implementation.API Contract Drafting
An API contractA formal specification of what an API endpoint accepts (request shape) and returns (response shape, status codes, error formats). Defined before implementation begins so frontend and backend can develop in parallel without breaking each other. is the agreement between the person building the endpoint and the person calling it. Define it in OpenAPI 3.1The current standard for REST API specification, formerly known as Swagger. OpenAPI YAML/JSON describes every endpoint: URL, method, request body schema, response schema, authentication, and error codes. FastAPI generates OpenAPI automatically from your routes — but writing it first forces you to think through the contract before coding. YAML before implementation and you can: generate frontend types, write integration tests, and hand it to a client team — all before the endpoint exists.
gemini "@routers/tasks.py @schemas.py @spec/recurring-tasks.json
Draft the OpenAPI 3.1 YAML for these three new endpoints:
POST /tasks/{task_id}/recurrence
GET /tasks/{task_id}/recurrence
DELETE /tasks/{task_id}/recurrence
Follow the patterns in the existing TaskFlow routes:
- All endpoints require Bearer auth (JWT)
- Error responses use {detail: string} format (FastAPI default)
- 404 when task_id doesn't exist or doesn't belong to the current user
Output valid YAML only. Use $ref for reusable schemas." \
> docs/recurrence-api.yaml
HTTPException(status_code=404, detail="Task not found")) and the auth pattern (Depends(get_current_user)). It uses these to write a contract that matches what the implementation will actually return.WHY YAML FIRST: If the frontend team receives the OpenAPI spec today, they can start building the recurrence settings UI while you implement the endpoint — true parallel development.
OpenAPI Output
openapi: "3.1.0"
info:
title: TaskFlow Recurrence API
version: "1.0"
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
RecurringTaskCreate:
type: object
required: [schedule_type]
properties:
schedule_type:
type: string
enum: [daily, weekly, monthly]
description: "How often the task recurs"
interval_value:
type: integer
nullable: true
description: "e.g. every 2 weeks — set schedule_type=weekly, interval_value=2"
RecurringTask:
type: object
properties:
id: { type: integer }
task_id: { type: integer }
schedule_type: { type: string, enum: [daily, weekly, monthly] }
interval_value: { type: integer, nullable: true }
next_run_at: { type: string, format: date-time }
is_active: { type: boolean }
created_at: { type: string, format: date-time }
HTTPError:
type: object
properties:
detail: { type: string }
paths:
/tasks/{task_id}/recurrence:
post:
summary: Set a recurrence schedule on a task
security: [{ BearerAuth: [] }]
parameters:
- name: task_id
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/RecurringTaskCreate' }
responses:
"201":
description: Recurrence created
content:
application/json:
schema: { $ref: '#/components/schemas/RecurringTask' }
"404":
description: Task not found
content:
application/json:
schema: { $ref: '#/components/schemas/HTTPError' }
"401":
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/HTTPError' }
get:
summary: Get recurrence settings for a task
security: [{ BearerAuth: [] }]
parameters:
- name: task_id
in: path
required: true
schema: { type: integer }
responses:
"200":
description: Recurrence settings
content:
application/json:
schema: { $ref: '#/components/schemas/RecurringTask' }
"404":
description: Task not found or no recurrence set
delete:
summary: Remove recurrence from a task
security: [{ BearerAuth: [] }]
parameters:
- name: task_id
in: path
required: true
schema: { type: integer }
responses:
"204":
description: Recurrence deleted
"404":
description: Task or recurrence not found
$ref for reusable schemas — a best practice it inferred from the existing pattern of reusing Pydantic schemas across routes. The HTTPError schema matches FastAPI's actual error format exactly.On a 3-person team (1 backend, 1 frontend, 1 QA), the OpenAPI spec enables: frontend starts building the UI against the contract today; QA writes integration tests against the spec today; backend implements the endpoint this week. All three work in parallel. Without the spec, everyone blocks on the backend developer finishing first.
Database Schema Design
We've designed the SQLAlchemy model. Now we need the Alembic migrationAlembic is a database migration tool for SQLAlchemy. A migration script describes changes to the database schema (add table, add column, drop index) so those changes can be applied incrementally to production databases without losing existing data. script that creates the recurring_tasks table in the existing database — without touching any existing data.
gemini "@models.py @database.py
Design the Alembic migration script to add the recurring_tasks table.
The table needs these columns (match the RecurringTask model patterns):
- id, task_id (FK + UNIQUE), schedule_type, interval_value, next_run_at, is_active, created_at
Requirements:
- The upgrade() function adds the table and FK constraint
- The downgrade() function drops the table cleanly
- Use op.create_table() and op.drop_table() (Alembic style)
- Handle the CASCADE: deleting a task should delete its recurrence
Output only the migration file content."
Generated Migration Script
"""add recurring_tasks table
Revision ID: 001_recurring
Revises:
Create Date: 2026-06-10
"""
from alembic import op
import sqlalchemy as sa
revision = '001_recurring'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
'recurring_tasks',
sa.Column('id', sa.Integer(), primary_key=True, index=True),
sa.Column('task_id', sa.Integer(),
sa.ForeignKey('tasks.id', ondelete='CASCADE'),
unique=True, nullable=False),
sa.Column('schedule_type', sa.String(), nullable=False),
sa.Column('interval_value', sa.Integer(), nullable=True),
sa.Column('next_run_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('is_active', sa.Boolean(), server_default='1'),
sa.Column('created_at', sa.DateTime(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP')),
)
op.create_index('ix_recurring_tasks_task_id', 'recurring_tasks', ['task_id'])
def downgrade() -> None:
op.drop_index('ix_recurring_tasks_task_id', table_name='recurring_tasks')
op.drop_table('recurring_tasks')
ondelete='CASCADE' to the FK — so deleting a task automatically deletes its recurrence. It learned this from the cascade="all, delete-orphan" in the SQLAlchemy relationship. The server_default=sa.text('CURRENT_TIMESTAMP') matches the func.now() pattern in the model.GOTCHA: Alembic isn't installed in TaskFlow's current requirements.txt. You'll need
pip install alembic and alembic init alembic before running this migration. Add it to requirements.txt.Architecture Decision Records (ADR)
From the M05 gap analysis, we surfaced Open Question OQ-1: "Who triggers the spawn — a background scheduler or an endpoint call?" This is an architectural decisionA choice that constrains how the system is built and is hard to reverse. Architectural decisions include: what database to use, how to handle background work, synchronous vs. asynchronous processing. These should be documented so future engineers understand why the system is built the way it is.. Record it before implementing, because the choice shapes the entire implementation.
gemini "@models.py @routers/tasks.py @spec/recurring-tasks.json
Write an Architecture Decision Record (ADR) for the following decision:
Decision: How should recurring task spawning be triggered?
Option A: Cron expression (server-side background scheduler polls every minute)
Option B: Interval-based (client calls POST /tasks/{id}/spawn on a schedule)
Option C: Event-driven (SQLAlchemy event listener on Task.completed change)
For each option: list pros, cons, and fit with TaskFlow's current architecture.
Include: Context, Decision Drivers, Considered Options, Decision Outcome,
Consequences (positive and negative).
Use Michael Nygard's ADR template format." > docs/adr-001-recurrence-trigger.md
WHY ADRs: In 6 months, when a new developer asks "why don't we just use SQLAlchemy events?", the ADR has the answer. Without it, the team debates the decision again and might reverse it without understanding why it was made.
ADR Output
# ADR-001: Recurring Task Spawn Trigger Mechanism
## Status
Accepted
## Context
TaskFlow (FastAPI + SQLite) needs to auto-spawn new tasks when a recurring
task is completed. Three mechanisms were considered. The system currently
has no background workers, no message queue, and no event system.
## Decision Drivers
- Simplicity: TaskFlow is a single-server app; no Celery or Redis
- Reliability: tasks must spawn even if the server restarts
- Consistency with existing architecture
## Considered Options
**Option A: Background Scheduler (APScheduler)**
- Pros: server-side control, survives client disconnects, predictable timing
- Cons: adds dependency, must handle scheduler restart on server boot
- Fit: poor for serverless but good for always-on uvicorn deployment
**Option B: Client-triggered endpoint**
- Pros: zero new infrastructure, simplest implementation
- Cons: relies on clients to call reliably — breaks if client is offline
- Fit: bad for a task manager (tasks should recur even if user is inactive)
**Option C: SQLAlchemy event listener on Task.completed**
- Pros: automatic, no polling, fires synchronously on task update
- Cons: synchronous spawn blocks the PATCH /tasks/{id} response;
tested in tasks.py but not in scheduler tests
- Fit: viable but mixes spawning logic with the update handler
## Decision Outcome
**Chosen: Option A — APScheduler background scheduler**
Reasoning: TaskFlow runs as a persistent uvicorn process. A scheduler
polling every 60 seconds is invisible to users, survives client disconnects,
and doesn't block the PATCH /tasks/{id} response. The added complexity
(one new dependency, scheduler startup in main.py) is justified by
reliability requirements from FR-3 ("within 60 seconds").
## Positive Consequences
- Recurring tasks spawn reliably regardless of client activity
- PATCH /tasks/{id} response time is unaffected
## Negative Consequences
- APScheduler must be added to requirements.txt
- Tests must mock the scheduler or use a test-only trigger endpoint
- If the server process restarts, the scheduler must re-initialize
(mitigation: store next_run_at in the DB, not memory)
C4 Diagram: TaskFlow's Architecture
A C4 modelA hierarchical architecture visualization framework by Simon Brown: Context (who uses the system), Containers (what processes/databases exist), Components (what code modules exist inside a container), Code (class-level detail). Start at Context and drill down only as needed. gives different levels of zoom for different audiences. Your manager needs the Context level. A new backend developer needs the Component level. Gemini can generate all three from TaskFlow's source files.
gemini "@GEMINI.md @main.py @routers/ @models.py
Generate a C4 model for TaskFlow at the Context and Container levels.
Use Mermaid C4Context and C4Container diagram syntax.
Context level: who are the users, what external systems exist?
Container level: what processes run (FastAPI app, SQLite DB, Scheduler)?
Show: technology labels, relationships, data flows.
After the Mermaid code, add a 3-sentence plain-English summary
of TaskFlow's architecture for a new team member." > docs/architecture-c4.md
In this section you generated: a codebase-aware SQLAlchemy model, an Alembic migration script, an OpenAPI 3.1 spec, an ER diagram, a sequence diagram, a C4 architecture model, and an Architecture Decision Record. All from the actual TaskFlow source files. In a traditional SDLC, these artifacts take 1-2 days of design work. With Gemini CLI, they take under an hour — and they're grounded in your real code, not generic examples.
Lab: Design the Recurring Tasks Feature
Prerequisite: complete the M05 lab so spec/recurring-tasks.json exists. By the end you'll have a design document ready for M07 implementation.
cd sample-project\taskflow
New-Item -ItemType Directory -Force -Path docs
gemini "@models.py @spec/recurring-tasks.json
Generate a Mermaid erDiagram including the new recurring_tasks table.
Show all existing tables (users, tasks, tags, task_tags) plus
recurring_tasks with all fields and relationship lines." > docs/er-diagram.md
cat docs/er-diagram.md
gemini "@models.py @database.py @spec/recurring-tasks.json
Design the RecurringTask SQLAlchemy model following existing patterns.
Include the modification to the Task class.
Output only Python code." > docs/recurring-task-model.py
# Review the output
cat docs/recurring-task-model.py
gemini "@routers/tasks.py @schemas.py @spec/recurring-tasks.json
Draft OpenAPI 3.1 YAML for POST/GET/DELETE /tasks/{task_id}/recurrence.
Match TaskFlow's auth pattern and error format. Output YAML only." \
> docs/recurrence-api.yaml
# Validate YAML syntax (requires PyYAML)
python -c "import yaml; yaml.safe_load(open('docs/recurrence-api.yaml')); print('Valid YAML!')"
gemini "@models.py @routers/tasks.py @spec/recurring-tasks.json
Write an ADR for how to trigger recurring task spawns:
Option A: APScheduler background scheduler
Option B: Client-triggered endpoint
Option C: SQLAlchemy event listener
Use Michael Nygard ADR format. Recommend one option with reasoning." \
> docs/adr-001-recurrence-trigger.md
echo "Design complete. Review docs/ directory:"
ls docs/
You should see: er-diagram.md, recurring-task-model.py, recurrence-api.yaml, adr-001-recurrence-trigger.md. These four artifacts are your complete design document. M07 implementation will use them.
Quiz
server_default=func.now() for the created_at column. Why did it choose this over default=datetime.utcnow?models.py where all existing timestamp columns use server_default=func.now()datetime.utcnow is not supported in SQLAlchemy 2.0server_default is required for timezone-aware columns/docs pageondelete='CASCADE' on the task_id FK. Where did Gemini learn to add this?cascade="all, delete-orphan" on the User.tasks relationship in models.py, inferring the same behavior was intendedauth.py relates to routers/tasks.py?