Nobody has started this yet — be first.
Business impact
POST /notes has no uniqueness check on title at all. Two, or two hundred, notes can share the exact same title (even the exact same case) with no error. There's also no protection at the database level -- Note.title has no unique constraint. This is the one ticket in this set that's genuinely about correctness under concurrency, not just correctness for a single request in isolation: a slow UI where a user double-clicks "Save," or two automated scripts racing to create a scheduled note, both need to end up with exactly one note, not two.
Problem
POST /notes accepts any number of notes sharing the exact same title, with no case-insensitive (or case-sensitive) uniqueness check anywhere in the request path, and no unique constraint at the database level to fall back on.
Current behavior
POST /notes accepts any number of notes sharing the exact same title (even identical case), and under N simultaneous requests for the same new title, every single one succeeds with 201 instead of exactly one winning and the rest getting 409.
Expected behavior
POST /notes rejects a title that already exists on another note, case-insensitively ("Weekly Report" and "weekly report" count as the same title), with 409 Conflict. This must hold even when many requests for the same new title arrive at approximately the same instant -- at most one may succeed, the rest must get 409, never two notes with the same title.
Steps to reproduce
cd fastapi/notes_api
source .venv/bin/activate
uvicorn app.main:app --reload &
curl -s -X POST http://127.0.0.1:8000/notes -H "Content-Type: application/json"
-d '{"title":"Weekly Report","content":"v1"}'
curl -s -X POST http://127.0.0.1:8000/notes -H "Content-Type: application/json"
-d '{"title":"weekly report","content":"v2"}'
Why this matters
It's tempting to implement this as: "query for a note with this title; if none exists, insert." That's straightforward, reads clearly, and is wrong under concurrency: two requests can both run the SELECT and both see "no existing match" before either one commits its INSERT -- both then insert, and you're back to duplicates, just less often (only under contention, which makes it far worse in production than in casual manual testing). A check-then-act pattern like this is a textbook TOCTOU (time-of-check to time-of-use) race condition -- which is exactly why this ticket's test wraps genuinely concurrent requests in a 15-second timeout and uses a file-backed (not in-memory) SQLite database, so real, separate connections actually race each other instead of the race being masked by shared in-process state.
Suggested approach
Think about what actually guarantees uniqueness under concurrent writers in a relational database: it's the database's own unique constraint (and the transaction/locking machinery behind it), not anything checked in application code beforehand. Look at how Note is defined in app/models.py -- a unique=True (or an explicit UniqueConstraint) on the right column enforces this at the database level regardless of how many concurrent requests are in flight. From there, create_note needs to handle the case where the database rejects the insert (SQLAlchemy raises IntegrityError on a unique constraint violation) and translate that into a 409 HTTP response, instead of letting a 500 leak out. Also think about case-insensitivity: a plain unique constraint on title as-stored would treat "Report" and "report" as different values -- the uniqueness check itself needs to be case-insensitive.
Acceptance criteria
Verification
.venv/bin/pytest practicetickets/test_ticket07_duplicate_title_race.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b feat/duplicate-title-raceFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/duplicate-title-raceSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign in./data/notes.dbOr run it via Docker instead:
cd fastapi/notes_api
docker compose up --build # API on http://127.0.0.1:8000, /docs included
SQLite data is written inside the container to /app/data/notes.db, backed by the notes-data named volume declared in docker-compose.yml -- it survives docker compose restart and re-running docker compose up after down (without -v).
Work the tickets in practicetickets/ (ticket01 through ticket07); each names one pytest file in the same directory, separate from the project's own tests/ suite:
.venv/bin/pytest practicetickets/test_ticket01_tag_length_boundary.py -v # a single ticket
./practicetickets/run_tickets.sh # all 7, pass/fail summary
./practicetickets/run_tickets.sh -v # summary + full output
Note: ticket 02's bug also breaks two pre-existing tests in tests/test_notes.py (test_search_by_q_matches_title_and_content_case_insensitively and test_filter_by_tag_and_q_are_combinable) -- that's expected, and fixing ticket 02 should bring tests/ back to fully green. Run the main suite with .venv/bin/pytest tests/ -v.
Level 2
Implement a feature
Extend the system within its own patterns.