Build a private bookmark API with ownership and version checks
Application background
Ana finds an article on her phone and taps Save. Later she opens the learning portal on her laptop and expects to find the same link. The portal therefore needs to store bookmarks in a database, rather than only in one browser.
Each bookmark belongs to one person. Ana should see her own links, and Ben should see his. Another problem appears when Ana leaves the same bookmark open on two devices and edits both copies.
Example walkthrough
01 · Try this input
- Input / starting state
- Ana saves
https://example.com/sql - Expected result
- Return a bookmark ID and show the link in Ana's list.
02 · Try this input
- Input / starting state
- Ben asks for his list
- Expected result
- Do not include Ana's private bookmark.
03 · Try this input
- Input / starting state
- Two devices edit bookmark 41 from version 3
- Expected result
- Accept one edit as version 4. Tell the second device its copy is out of date.
A version is a number attached to a saved record. Checking it before an edit prevents an older browser view from silently replacing a newer change.
Sizing that affects this decision
10,000 users with 20 bookmarks each means 200,000 records. At an assumed 1 KiB of metadata per record, that is about 195 MiB of raw data before indexes and backups. The initial target of 100 peak reads/s and 20 writes/s supports starting with one regional database and measuring it. It does not, on its own, justify adding a cache.
These are exercise assumptions. The estimation reference explains the units and approximations. They do not establish the local demo's measured capacity.
Your assignment
Deliver: Build endpoints to save, list and edit private bookmarks. Keep the records across restarts, enforce their owner and report when an edit is based on an old version.
Required behavior: POST /bookmarks returns 201 and a version. GET /bookmarks returns only the authenticated owner’s rows. PATCH /bookmarks/{id} requires expected_version and returns 409 on conflict. Sharing is a later feature.
The required first milestone is a working local implementation of the behavior above. The numbered implementation steps define the scope. The cloud architecture is a later extension, not something the starter has already provisioned.
Get the code and run the supplied example
The code is in the public junior-to-staff repository. Install Git and Python 3.12+. No AWS account or Python packages are required for this first run. If you already have a checkout, use it and skip cloning.
git clone https://github.com/Soulful-Iris/junior-to-staff.git
cd junior-to-staff
python3 examples/architecture-starts/bookmark_service.py
Supplied file: examples/architecture-starts/bookmark_service.py. You can also read or download the source here (download file, source below).
Read the supplied code · bookmark_service.py
"""Local mechanism demonstration for bookmark-service. No AWS resources are created."""
import sqlite3
c=sqlite3.connect(':memory:')
c.execute('CREATE TABLE bookmarks(owner TEXT,id INTEGER,title TEXT,version INTEGER,PRIMARY KEY(owner,id))')
c.execute("INSERT INTO bookmarks VALUES ('ana',7,'Original',3)")
for owner,title,version in [('ben','Unauthorized',3),('ana','Phone edit',3),('ana','Laptop edit',3)]:
changed=c.execute('UPDATE bookmarks SET title=?,version=version+1 WHERE owner=? AND id=7 AND version=?',(title,owner,version)).rowcount
print(owner,title,'saved' if changed else 'not found or version conflict')
print('Final:',c.execute('SELECT * FROM bookmarks').fetchall())
This program is a mechanism demonstration: it runs the small scenario in one process and prints the result. It is not an HTTP service, a complete application, or an AWS deployment. A successful run demonstrates this mechanism only. It does not establish the workload or failure guarantees of the application you will build.
Example output from the supplied run:
Generated IDs and timestamps may differ. Compare the state transitions and outcomes.
ben Unauthorized not found or version conflict
ana Phone edit saved
ana Laptop edit not found or version conflict
Final: [('ana', 7, 'Phone edit', 4)]
Set up your implementation workspace
Create work/bookmark-service/ in your checkout (or use a separate repository). Copy the supplied mechanism into that directory as mechanism.py, then extract its state transitions into functions you can call from your implementation. The record and module names below describe what you must implement. They are not a promise that files with those names already exist. Keep a README.md beside your implementation with its exact run commands and observed results.
Local components and state to implement
This table names the records, interfaces or decision inputs for your deliverable. Unless a name is explicitly linked to supplied source above, it is something you create. Implement the local state transitions first, then connect the HTTP, storage or worker boundaries required by the steps.
| Record / module | Key or interface | Responsibility |
|---|---|---|
| bookmarks | (owner_id, bookmark_id), version | Owns the URL, title and current revision. |
| operations | (owner_id, request_id), payload_hash | An exact retry returns the same result. Changed payload under the same key conflicts. |
| api.py / store.py | create, list_owned, edit_if_version | Derive owner from verified identity. Enforce it in every database operation. |
Implement the assignment
1. Create and list one private record
Implement api.py with create/list routes and a trusted request context containing the authenticated subject. In store.py, scope both lookup and update by owner. Save a URL without fetching it. An unavailable title service must not lose the bookmark. Show Ana’s list and Ben’s empty list for the same ID.
2. Preserve a client’s edit
Keep expected_version and the user’s unsaved draft in the browser. Commit UPDATE ... WHERE owner_id=? AND id=? AND version=?. Zero affected rows require a fresh read to distinguish missing from conflicting under your disclosure policy. The losing client keeps its draft and sees the latest saved version.
3. Make create retries safe
Commit the bookmark and operation result in one transaction. Compare a canonical payload hash when a request ID reappears. Crash after commit but before replying, then repeat the same request and show the original ID. Retain operation IDs for the documented retry window.
4. Add the cloud adapter
Map owner to the DynamoDB partition and bookmark ID to the sort key. Use a conditional version update and strongly consistent reads for the immediate post-write list. Keep the local SQL and cloud code behind the same store interface. Add cursor pagination with a stable ordering before creating a large inventory.
Demonstrate the completed local result
01 · Try this input
- Input / starting state
- Run the starting program
- Expected result
- Ben changes zero rows. Ana’s first edit succeeds. Her second version-3 edit conflicts.
02 · Try this input
- Input / starting state
- Retry a committed create
- Expected result
- One bookmark and the original response ID.
03 · Try this input
- Input / starting state
- Open the losing browser draft
- Expected result
- The draft remains visible beside the current server version.
Handoff: In your implementation README, include the start command, one successful operation, the failure case above and the resulting stored state or decision. State which dependencies are simulated. Someone with a fresh checkout should be able to reproduce this without your chat history.
Workload assumptions and capacity decisions
These are constructed exercise assumptions. The stated workload is a design target. The local demonstration does not establish that throughput. Use the estimation constants to check units before choosing capacity.
| Input or objective | Calculation / consequence |
|---|---|
| 10,000 users × 20 bookmarks | 200,000 rows. At 1 KiB per row, about 195 MiB before indexes and backups. |
| 100 peak reads/s. 20 writes/s | Start with a single regional database. A cache is an additional consistency problem, not a prerequisite. |
| Create/list p95 under 300 ms | Measure the complete request. Reserve time for authentication, database work and response transfer. |
Map the local implementation to AWS
Deployment status: local only. Running the supplied command creates no AWS resources and configures no cloud connections. The diagram is a proposed deployment of the completed application. Each box needs either a deployed runtime, a provisioned service or an explicitly external dependency.
Read the diagram by following the arrows from the entry point: application code accepts the request or event, the state owner commits it, and any worker produces the later result. The table ties those roles to code and adapter work. Multiple boxes do not imply multiple Python files already exist.
DynamoDB is a good fit for owner-keyed lists and conditional edits. PostgreSQL remains a simpler choice when sharing and relational queries dominate. Cognito authenticates the caller. Application code still decides who owns each record.
| Local responsibility | Cloud destination and role | Implementation still required |
|---|---|---|
| Local HTTP boundary or the endpoint you will add | Amazon API Gateway: authenticated HTTP entry | Create routes and an integration. Translate requests and responses and configure identity validation. |
| Python operation or worker function | AWS Lambda: bookmark application | Write a Lambda event adapter, package its dependencies and give its role only the required resource actions. |
| Local fixture identity or caller supplied to the operation | Amazon Cognito: identity provider | Configure an identity provider and validate tokens. Retain resource ownership checks in application code. |
| Local dictionary, SQLite records or state model | Amazon DynamoDB: private bookmark store | Design partition/sort keys and write a storage adapter with conditional updates or transactions. Python state and SQL are not uploaded as a database. |
Provision resources, then connect the application
| Resource or boundary | Initial configuration and reason |
|---|---|
| API Gateway + Lambda | Set application deadline 2 s and a short database timeout. Validate the JWT issuer/audience and required claims. |
| DynamoDB | Owner-prefixed keys. Conditional version changes. Point-in-time recovery. Do not use a stale secondary index to promise immediate read-your-writes. |
| CloudFront + S3 | Serve the static UI. Keep private API responses out of shared response caches. |
| IAM and logs | API role accesses the bookmark table only. Do not put private URLs or tokens in request logs. |
Use one disposable AWS environment for the cloud exercise. Put the named resources in infra/template.yaml or your existing IaC tool, pass resource IDs through configuration, and scope each runtime role to its own tables, buckets and queues. The diagram is a design to implement. It is not a claim that these resources have been deployed. Record the commands you used to deploy and remove the exercise resources.
For concrete provisioning commands, configuration wiring and cleanup, use the AWS foundation guide. It includes a deployable table/queue/object-storage foundation and explains which application and service adapters you still implement.
A provisioned queue or table does not make the local program use it. Configure resource IDs in the deployed runtime, replace the local adapter, and replay the same successful and failing operation against that runtime. Record the deployed commit and observable result, then remove the disposable resources using your infrastructure tool.
Extend the design after the baseline works
Worked follow-up: Revoke a shared bookmark even when its content is cached
Alice shares item 41 with Bob, then withdraws the share. A cached copy can still contain the correct bookmark text while carrying an obsolete access decision. Content freshness and permission freshness now have different owners.
| Starting design | Changed requirement |
|---|---|
| Only an authenticated owner can read a bookmark. | A recipient can read through a share token until the owner revokes it. |
Revised architecture. Follow the changed responsibility and failure path below. This is a design to implement. The supplied local example does not provision these components.
What to implement. Add a share record with token hash, item ID, expiry and revoked state. In the read handler, check the share against authoritative state before serving cached content. Keep private responses out of browser and CDN response caches. For the immediate-revocation contract, deny new reads when the permission store cannot be reached. A response already delivered cannot be recalled. On AWS, keep this decision in the API handler with a suitable consistent database read. CloudFront may still cache the static app.
Walk through the result. Warm token S by reading item 41. Revoke S, leave the content cache untouched, then repeat the same request. It must be denied. Document the ordering point: revocation completed before the new authorization check. Your deliverable is the share schema, revised read path and this request transcript.
Add revocable sharing only after private reads work. State the authorization decision point, recheck it on cache hits, and demonstrate that a revoked share token cannot authorize a new response. At ten times the read rate, measure query latency and hot owners before adding a cache.
Additional design cases, alternatives and original source notes
All prompts here are constructed practice, without company attribution.
Candidate opening: “Ana saves a private bookmark and immediately lists her bookmarks. Ben must not see it. Both of Ana's devices try to edit version 3. Preserve ownership and show one conflict without losing either device's draft.”
01 · Try this input
- Input / starting state
- Ana creates URL, then lists
- Expected result
- New ID appears in Ana's list
Scope: Read-your-writes required
02 · Try this input
- Input / starting state
- Ben reads Ana's ID
- Expected result
- Denied
Scope: Derive owner from authenticated identity
03 · Try this input
- Input / starting state
- Two PATCH requests expect v3
- Expected result
- One v4. One 409
Scope: No silent overwrite
Prerequisites: API/transaction concepts. This is a build brief. start from the full-stack exercise. First deliver one owner-scoped create/list, then conditional edits, then a retained conflict state. Run that exercise's commands and record visible behavior.
Before the worked design, name the invariant and move authority out of the submitted payload. Then trace a commit whose HTTP response disappears. Operation identity must survive a retry.
Worked approach and AWS mapping — open after your attempt
Prompt: save, list, and edit private bookmarks. Start with 10,000 users, 20 bookmarks each, and 100 peak reads/s. These are exercise assumptions. Require read-your-writes and owner-only access. Exclude sharing and search initially.
API: POST bookmark, GET own bookmarks with cursor, PATCH by id plus expected version. Data: id, owner, url, title, created_at, version. Enforce URL scheme rules. Do not fetch arbitrary submitted URLs in the API process.
AWS choice: API Gateway and Lambda for a small intermittent API. DynamoDB with owner partition and ordered keys for fixed queries. RDS PostgreSQL is a reasonable alternative for relational needs. Cognito or another identity provider authenticates. Ownership checks still live in your application. For TypeScript UI delivery, S3 and CloudFront can serve static assets.
Worked decision: do not cache private reads initially at this scale. First measure the actual query. A cache adds invalidation and tenant-isolation obligations before there is evidence it is necessary.
Implement: full-stack exercise, then AWS lab 1. Break: read another owner's id. Concurrently PATCH version 3 twice. Expected: access denied and exactly one conditional write succeeds. Senior extension: add sharing with revocation. Staff extension: roll out a new authorization model across old clients and background jobs.
Follow-up: sharing becomes revocable
Predict the warm-cache path before adding a CDN. A token-specific cache key alone does not recheck a revoked token.
Use the warm/cold revocation fixture for immediate or explicitly bounded authorization. Senior: test a repeated warmed token after revocation and a version conflict. Lead follow-up: old clients do not send the new version field. Stage compatibility and a rollout stop criterion instead of silently accepting blind overwrites. Assessor evidence is the denied cross-owner request, one winner/one conflict, and a stated revocation bound—not merely a diagram containing “auth.”