⌂ Home Gemini CLI Mastery — AI SDLC Track
Track 3 — AI SDLC
📚 Module 7 of 18 ⏰ 50 min
M06 — Design & Architecture

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 RecurringTask fits into TaskFlow's existing tables
  • Draft an OpenAPI 3.1 spec for the three new recurrence endpoints
  • Design the RecurringTask SQLAlchemy 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.

Analogy — The Renovation Architect

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:

PowerShell / Terminal — from sample-project/taskflow/
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."
WHAT: We load @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

Gemini Output — RecurringTask model addition for models.py
# ── 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",
)
NOTICE THE PATTERNS: Gemini matched 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

PowerShell / Terminal
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

docs/er-diagram.md — Mermaid output
```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"
```
HOW TO RENDER: Paste this into any GitHub markdown file, a Notion page, or the Mermaid Live Editor at mermaid.live. The ||--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.
TaskFlow Entity Relationships — Before and After Recurring Tasks
users
idINT PK
emailstr UNIQUE
usernamestr UNIQUE
is_activebool
─── owns ───>
tasks
idINT PK
titlestr
completedbool
prioritystr
owner_idINT FK
─ has ──>
recurring_tasks NEW
idINT PK
task_idINT FK UNIQUE
schedule_typestr
next_run_atdatetime
is_activebool

Sequence Diagram: Task Completion Flow

PowerShell / Terminal
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."
WHY THIS IS POWERFUL: Gemini reads the actual 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.

PowerShell / Terminal
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
WHAT: Gemini reads the existing router to understand FastAPI's error format (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

docs/recurrence-api.yaml (abbreviated)
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
WHAT JUST HAPPENED: Gemini produced a complete, valid OpenAPI 3.1 spec using $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.
Why It Matters — Parallel Development

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.

PowerShell / Terminal
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

alembic/versions/001_add_recurring_tasks.py
"""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')
PATTERN MATCHING: Gemini added 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.

PowerShell / Terminal
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
WHAT: Gemini reads the actual router code to understand the current architecture (no event system, no background tasks, no async workers) before evaluating options. This produces an ADR grounded in the real constraints of this codebase — not generic trade-offs.
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

docs/adr-001-recurrence-trigger.md
# 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)
ADR Template — What Each Section Captures
01
Status
Proposed / Accepted / Deprecated. Prevents treating old decisions as current.
02
Context
Why the decision was needed. Future engineers understand the constraints that existed at the time.
03
Considered Options
Alternatives evaluated with honest pros/cons. Shows this was a deliberate choice, not the first thing tried.
04
Decision Outcome
The chosen option and the explicit reasoning. This is the most important section.
05
Consequences
Both positive AND negative. Honest about trade-offs — including the mitigation strategies.

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.

PowerShell / Terminal
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
C4 Model — Four Levels of Zoom
Level 1: Context
Who uses TaskFlow and what external systems does it integrate with? Audience: stakeholders, managers.
Scope: TaskFlow app + end users. No technology labels at this level.
Level 2: Container
What major processes run? FastAPI app (Python/uvicorn), SQLite DB, APScheduler background worker. Audience: architects, senior devs.
Scope: processes, databases, external services with technology labels.
Level 3: Component
What modules exist inside the FastAPI container? routers/tasks.py, routers/users.py, auth.py, database.py. Audience: backend developers.
Scope: Python modules and their responsibilities.
Level 4: Code
Class-level detail for a specific component. Usually auto-generated from code. Audience: individual developers working on that component.
Scope: classes, functions, relationships within one module.
What Just Happened?

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

LAB From JSON Spec to Complete Design Document

Prerequisite: complete the M05 lab so spec/recurring-tasks.json exists. By the end you'll have a design document ready for M07 implementation.

1
Create the docs directory
PowerShell
cd sample-project\taskflow
New-Item -ItemType Directory -Force -Path docs
2
Generate the ER Diagram
PowerShell / Terminal
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
3
Design the RecurringTask Model
PowerShell / Terminal
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
4
Draft the OpenAPI Spec
PowerShell / Terminal
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!')"
5
Write the ADR
PowerShell / Terminal
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

Check Your Understanding 0 / 5
1. Gemini generated a RecurringTask model using server_default=func.now() for the created_at column. Why did it choose this over default=datetime.utcnow?
A. Gemini prefers SQLAlchemy-native functions over Python functions
B. It matched the pattern from models.py where all existing timestamp columns use server_default=func.now()
C. datetime.utcnow is not supported in SQLAlchemy 2.0
D. server_default is required for timezone-aware columns
2. Why should an API contract (OpenAPI spec) be written before the endpoint is implemented?
A. FastAPI requires an OpenAPI spec to generate the /docs page
B. It enables frontend, QA, and backend to work in parallel and prevents last-minute API changes from blocking other teams
C. Gemini CLI can only generate code from OpenAPI specs, not from markdown
D. OpenAPI specs are required for Alembic migrations to run correctly
3. The Alembic migration uses ondelete='CASCADE' on the task_id FK. Where did Gemini learn to add this?
A. From the OpenAPI spec which specified cascade behavior
B. From the cascade="all, delete-orphan" on the User.tasks relationship in models.py, inferring the same behavior was intended
C. Cascade is always required on ForeignKey columns in SQLite
D. Gemini adds cascade by default to all generated migrations
4. What is the primary purpose of the "Negative Consequences" section in an ADR?
A. To argue against the chosen decision and show it was the wrong choice
B. To be honest about the trade-offs accepted and document mitigations — so future engineers don't unknowingly re-open the decision
C. To provide alternatives for team members who disagree with the decision
D. Required for compliance and legal review of architecture decisions
5. At which C4 level would a new backend developer look to understand how auth.py relates to routers/tasks.py?
A. Level 1 (Context) — shows all user interactions
B. Level 2 (Container) — shows the FastAPI process and SQLite database
C. Level 3 (Component) — shows modules within the FastAPI container and their relationships
D. Level 4 (Code) — shows class-level detail