Admin — Managing Teams & Seasons
Premier League Predictor: FastAPI & Redis
Chapter 3 · Admin: Managing Teams & Seasons
Chapter 2 modeled teams, fixtures, and their real membership relationships. This chapter builds the real admin routes that actually create and manage them — and finds both a genuine simplicity win for Redis and a sharper, more surprising limitation than anything Chapter 2 turned up.
The Current Season: One Key, Not a Cross-Row Clear
This project's own real PostgreSQL and SQLite siblings both had to solve the same problem: only one
season is ever "current" at a time, so promoting a new one to current means clearing the flag on
every other row first. In Redis, that whole problem simply doesn't exist, because there's no
is_current field sitting on individual season records at all — there's one dedicated
key holding the current season's own ID directly:
Creating Teams: A Real Secondary Index, Not a UNIQUE Constraint
The PostgreSQL sibling can lean on a real UNIQUE constraint on a team's own name column
to guarantee a relegated-then-promoted club is reused as the same permanent record, never
accidentally duplicated. Redis hashes have no such thing — nothing stops two different team keys
from both holding name: "Arsenal". The real, idiomatic fix is a manually-maintained
secondary index: a second hash mapping name directly to ID.
A real create-team route checks this index first, and only creates a genuinely new hash and counter entry if nothing is found:
team:by_name and every team:{id} hash agree with each other. A route
that ever updates a team's own name directly, without also updating
team:by_name, would leave the index silently pointing at the old name forever. A real
SQL index on a name column would never need this kind of manual bookkeeping at all —
it's maintained by the database itself, on every write, automatically.
The 20-Team Cap: SCARD, Not a CHECK Constraint
Adding a team to a season means adding its ID to that season's own membership Set — and the real 20-team limit has to be enforced by the application, since Redis Sets have no native maximum-size option:
Verified against a real, freshly-populated season set:
The Real Headline Finding: MULTI/EXEC Does Not Roll Back
This course's own real siblings each lean on a genuine atomicity guarantee for multi-step admin
writes — SQLAlchemy's flush()-then-commit() pair, or a real
db.transaction() wrapper — where, if anything inside fails, every earlier change in
that same operation is undone as if it had never happened. Redis has its own real transaction
mechanism, MULTI/EXEC — but Redis's own official documentation states its
real guarantee directly: "Redis does not support rollbacks of transactions since supporting
rollbacks would have a significant impact on the simplicity and performance of Redis." A
command that fails inside a real Redis transaction doesn't undo the commands around it —
"even when a command fails, all the other commands in the queue are processed."
10 and a real Set (the wrong type for INCR),
then queuing three commands in one real redis-py transaction — two valid increments
around one command guaranteed to fail:
10. It ended at 12 — both valid
INCR calls genuinely took effect, permanently, despite the transaction as a whole
raising a real Python exception. This is the sharpest, most easily-misread part of the finding:
redis-py's own pipe.execute() genuinely raises an error, which reads
exactly like the kind of exception a SQL transaction context manager raises specifically to signal
"nothing was committed" — but here, raising an exception and rolling back are two completely
unrelated things. The exception just reports that one command failed; it says nothing about the
fate of the others, because Redis never undid them in the first place.
| Stack | On a mid-transaction failure |
|---|---|
| PostgreSQL / SQLAlchemy sibling | Every change in the same flush/commit is undone — a genuine all-or-nothing guarantee |
| SQLite / better-sqlite3 sibling | The real db.transaction() wrapper rolls back every change on any thrown error |
| This course (Redis) | Verified: earlier successful commands in the same MULTI/EXEC permanently survive; only the one failing command is skipped |
HSET, one SADD, one SET — rather than chaining
several writes together inside one transaction and trusting a rollback that Redis genuinely doesn't
provide. Where a real multi-step sequence is unavoidable, later chapters reach for Redis's own real
WATCH-based optimistic locking instead, which aborts an entire transaction before
any of it runs if a watched key changed — a genuinely different, and genuinely real, kind of safety
net than rollback, covered honestly once this course actually needs it.
Hands-On Exercises
Reproduce this chapter's own no-rollback finding with a genuinely different failing command -- queue a real HSET against a key that already holds a plain string (not a hash), sandwiched between two valid SADD calls on a real Set, inside one MULTI/EXEC transaction. Report whether the two valid SADD calls survive.
📄 View solutionCreate a real team, then simulate a route that renames it by calling HSET on team:{id} with a new name field -- WITHOUT updating team:by_name. Show, with real code, that the old name still resolves via the secondary index while the new name does not, and explain what a real user-facing symptom of this bug would look like.
📄 View solutionExplain, in your own words, why a Python exception raised by pipe.execute() cannot be safely treated as "this transaction had no effect," using this chapter's own verified counter example as evidence. What real, different question does the exception actually answer?
📄 View solutionChapter 3 Quick Reference
- Current season = one key — a genuine simplicity win, no cross-row clearing needed at all
- Team uniqueness needs a manual secondary index — team:by_name stands in for a real UNIQUE constraint, and the app owns keeping it in sync
- The 20-team cap is a manual SCARD check — no native CHECK-constraint equivalent
- Verified: MULTI/EXEC does not roll back — earlier successful commands in a failed transaction permanently survive
- An exception from execute() ≠ nothing happened — it reports one command's own failure, not the fate of the whole transaction