Nobody has started this yet — be first.
Business impact
Every link in the system is cached for the same global DEFAULT_CACHE_TTL_SECONDS (currently 3600s / 1 hour), with no way to tune it per-link. That is a problem in both directions: a link whose target_url changes frequently (a "redirect to whatever's trending" link, or one an admin edits often via the PATCH endpoint) stays stale in Redis for up to an hour after every edit; a link known to be extremely hot and completely static could safely be cached far longer than an hour to reduce DB load further, but cannot be today without changing the global default for every other link too.
Problem
create_short_url accepts target_url, custom_slug, and expires_in_days -- nothing about caching. resolve_short_url always calls set_cached_url(..., ttl=current_app.config["DEFAULT_CACHE_TTL_SECONDS"]) with the global default, unconditionally.
Current behavior
Requesting cache_ttl_seconds=5 for a link has no effect at all -- the field does not exist on the create endpoint, and the link's Redis cache entries still carry the global default TTL (3600s).
Expected behavior
POST /api/urls accepts an optional cache_ttl_seconds field. When provided, that value (not the global default) is used as the base TTL for that link's cache entries -- still subject to the existing rule that the TTL can never outlive the link's own expires_at (see the min-with-seconds-to-expiry logic already in app/utils/cache.py::set_cached_url).
Steps to reproduce
Why this matters
This needs a new column (cache_ttl_seconds, nullable -- None means "use the global default") and a migration for it, plus threading that value from create_short_url through to the set_cached_url call in resolve_short_url. It is a good exercise in extending an existing Alembic-managed schema without disturbing the existing click_count migration, and in making sure a per-row override composes correctly with the existing expiry-clamping logic rather than bypassing it.
Suggested approach
Look at how expires_in_days is validated and applied in create_short_url for the general shape (optional field, validate, store), and at ShortURL.expires_at for how an optional per-row value is modeled. Generate the migration the same way the existing ones were generated (flask db migrate -m "..." against a real Postgres, then review the autogenerated file) rather than hand-writing DDL.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_18_per_slug_ttl.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
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/per-slug-cache-ttl-overrideFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/per-slug-cache-ttl-overrideSubmit 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 indocker-entrypoint.shcurl -X POST http://localhost:5000/api/urls \
-H "Content-Type: application/json" \
-d '{"target_url": "https://anthropic.com", "custom_slug": "claude"}'
# -> {"slug": "claude", "short_url": "http://localhost:5000/claude", "expires_at": null}
curl -i http://localhost:5000/claude # 302 redirect, records a click
make seed # or: docker compose exec web flask seed-db
If something is already listening on 5432/6379 locally, uncomment POSTGRES_HOST_PORT / REDIS_HOST_PORT in .env to remap the host side — the app always talks to db/redis over the compose network regardless. Run production-style (gunicorn, no dev server, no bind mount) with docker compose -f docker-compose.prod.yml up --build. GET /healthz checks both Postgres and Redis connectivity.
You do not need Docker running at all to work the tickets — the test suite uses TestingConfig (SQLite in-memory via StaticPool) and fakeredis instead of a real Postgres/Redis, so everything runs against a plain virtualenv:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest practice_tickets/tests/test_ticket_01_base62_roundtrip.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 20, clean pass/fail summary
./practice_tickets/run_tickets.sh 05 13 16 # just a few, by number
All 20 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. .venv/bin/pytest tests/ (via make test) runs the project's own main suite — some ticket bugs (TICKET-05, 07, 08, 09, 14 among others) also break assertions in that suite, since they are real bugs in shared code paths; fixing the ticket fixes those too, not a separate task.
Level 2
Implement a feature
Extend the system within its own patterns.