Build a shared reading-list UI with private reading state
Application background
Alice and Bob belong to a study group. Alice saves a link and adds a note about which chapter is useful. Bob can browse that link and note, then mark the article as read for himself. Alice's reading status should remain unchanged.
The browser also needs to handle an edit made from an old view. If Alice changes her note on a laptop while the same note is open on her phone, the phone should not silently overwrite the newer text or discard its own draft.
The conflict response your interface must handle
The supplied local API accepts a note edit at PATCH /bookmarks/1. This body says the browser edited version 1:
{"version":1,"note":"Read the storage section"}
If the stored note is already version 2, the API returns 409 Conflict with:
{"error":"Version conflict; refresh before editing"}
Keep the typed note visible, fetch the current list and let the user decide how to reconcile the two versions. Do not replace the draft just because the request failed.
Shared bookmark data and personal reading state are different records. The supplied API provides those operations. Your main task here is the browser interface that makes the outcomes understandable.
Your assignment
Deliver: Build a browser interface over the supplied API. Keep each person's reading status separate and preserve unsaved note text when the API reports a version conflict.
Required behavior: Group membership controls link visibility. Shared link metadata and per-user read state are separate. Conditional edits return a conflict without discarding the user’s draft. Delayed responses cannot overwrite newer UI state.
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/a_shared_reading_list.py
Supplied file: examples/architecture-starts/a_shared_reading_list.py. You can also read or download the source here (download file, source below).
Read the supplied code · a_shared_reading_list.py
"""Local mechanism demonstration for a-shared-reading-list. No AWS resources are created."""
link={'version':3,'note':'Original'}; read_state={('ana','l7'):True,('ben','l7'):False}
def save(base,draft):
if base!=link['version']: return {'state':'conflict','draft':draft,'server':dict(link)}
link.update(version=base+1,note=draft); return {'state':'saved','server':dict(link)}
print(save(3,'Ana note')); print(save(3,'Ben draft')); print('Personal read state:',read_state)
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.
{'state': 'saved', 'server': {'version': 4, 'note': 'Ana note'}}
{'state': 'conflict', 'draft': 'Ben draft', 'server': {'version': 4, 'note': 'Ana note'}}
Personal read state: {('ana', 'l7'): True, ('ben', 'l7'): False}
Run the application you will extend
The reading-list API setup guide gives you a real local HTTP server, SQLite database, save/list/edit requests and controlled title success/timeout behavior. Start it in one terminal and send the documented curl requests from another. Read that setup before following the implementation steps below. The demo above isolates this lesson's mechanism. The server is where you integrate it.
For a first run, start this in terminal 1 from the repository root:
python3 examples/reading-list-starter/app.py --db /tmp/reading-list.sqlite3
In terminal 2, save one bookmark with a controlled title timeout:
curl -i http://127.0.0.1:8080/bookmarks \
-H 'X-Demo-User: alice' -H 'Content-Type: application/json' \
-d '{"url":"https://example.com/docs","title_mode":"timeout"}'
Expect 201 Created, a bookmark id and title_status: "timeout". The URL is persisted despite the title failure. This is the supplied baseline. The assignment adds the behavior described above. The lookup is a fixture, so no external website is contacted. For members Bob or Ben in a scenario, use the starter's second demo identity bob. Alice or Ana corresponds to alice.
Work in your own branch or copy examples/reading-list-starter/ to work/a-shared-reading-list/. app.py exists in that directory. Add the modules named below there as you separate HTTP, storage and background work. The server has demo membership, not production authentication.
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 |
|---|---|---|
| links | group,link_id,version,url,note | Shared metadata and conditional edit authority. |
| read_state | user,link_id,read_at | Personal state independent of other members. |
| editor_state | base_version,draft,server_value,request_generation | Explicit dirty/saving/conflict UI state. |
Implement the assignment
1. Build the group list first
Implement membership, create/list and stable link IDs. Save the URL before optional title enrichment. Paginate by a stable key and show loading, empty and unavailable states without clearing previously loaded content unnecessarily.
2. Separate personal interaction state
Store read/unread by authenticated user and link. Marking a link read changes only that user’s view. Optimistic UI should roll back or display a retry state after a failed write rather than pretending the server accepted it.
3. Implement an explicit editor state machine
Keep base version and draft separately from the latest server record. Save with expected version. On 409 show current server text beside the preserved draft. Use request generations so an older save response cannot replace a newer draft.
4. Handle membership and accessibility
Reauthorize API reads and writes after revocation, including warm caches. Provide keyboard-accessible controls and clear save/conflict announcements. Keep a revoked member’s local draft from being silently submitted under another user’s session.
Demonstrate the completed local result
| Action | Expected visible result |
|---|---|
| Run the starting program | Ana saves. Ben retains a conflicting draft. Their read states remain independent. |
| Deliver an older save response late | The newer local draft is not replaced. |
| Revoke membership | New list and save requests are denied even with cached group data. |
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 |
|---|---|
| Six members × 200 saved links | 1,200 potential personal read-state rows for 200 shared links. Do not put one global read flag on the link. |
| 50 concurrent active readers exercise peak | Begin with paginated database reads. A cache is optional after measuring. |
| Two edits from revision 3 | Exactly one revision-3 conditional update can win. The second client keeps its draft. |
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.
The UI needs its own explicit state model as much as the backend needs conditional writes. A successful conflict response is useful only if the browser preserves the work the user was trying to save.
| Local responsibility | Cloud destination and role | Implementation still required |
|---|---|---|
| Local static/media delivery path | Amazon CloudFront: reading-list web UI | Configure an origin, cache policy and private-content access. Distinguish cached bytes from current authorization. |
| Local HTTP boundary or the endpoint you will add | Amazon API Gateway: group API | Create routes and an integration. Translate requests and responses and configure identity validation. |
| Python operation or worker function | AWS Lambda: reading-list application | Write a Lambda event adapter, package its dependencies and give its role only the required resource actions. |
| Local dictionary, SQLite records or state model | Amazon DynamoDB: shared and personal state | Design partition/sort keys and write a storage adapter with conditional updates or transactions. Python state and SQL are not uploaded as a database. |
| Local fixture identity or caller supplied to the operation | Amazon Cognito: member identity | Configure an identity provider and validate tokens. Retain resource ownership checks in application code. |
| Local file, object fixture or exported payload | Amazon S3: static UI assets | Implement upload/download and metadata adapters, scoped access, object naming, retention and incomplete-upload cleanup. |
Provision resources, then connect the application
| Resource or boundary | Initial configuration and reason |
|---|---|
| API data | Include group scope in every key/query. Conditional versions on shared edits. |
| Frontend delivery | Cache versioned static assets. Keep private API responses out of shared caches. |
| Identity | Membership remains an application decision after token validation. Revocation affects current reads/writes. |
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
Add offline editing. Queue operations with base versions and identities, then surface genuine conflicts on reconnect instead of replaying last-write-wins over other members’ work.
Follow-up scenarios and worked designs
Follow-up 1 · Many groups
Alice belongs to two groups. Bob belongs to one. Which rows can Bob list?
Worked design and implementation
Filter by verified membership at the data access boundary. Never trust a client-supplied group ID alone. Test list, detail, edit and title-job paths for cross-group leakage.
Change the data access boundary. Store group membership independently from bookmark ownership. Every list, edit and background enrichment request derives the authorized group set from trusted membership. A submitted group ID selects within that set, it does not add membership. Include group identity in relevant cache and job keys.
Alice belongs to research and finance. Bob belongs only to research. Show Bob listing research successfully, then requesting a known finance bookmark ID and receiving no data. Remove Alice from finance while a title job waits and define whether that group-owned job may continue under a service identity. User departure and group data ownership are different decisions.
Follow-up 2 · The title provider stalls
Saving must return in 300 ms while the provider takes ten seconds. What moves?
Worked design and implementation
Atomically save the item and durable title job, return pending, and let a guarded bounded worker complete the title. Retry execution may repeat fetching. Conditional versions protect the current result.
Move waiting out of the HTTP handler. Save the bookmark with title_status: pending and a durable job in one transaction. Return the bookmark ID immediately. A dispatcher forwards committed intent to a bounded worker queue. The UI displays the URL while polling or subscribing for the later title. Completion checks the bookmark version so an old job cannot overwrite a user's edited URL.
Pause the title provider for ten seconds. Show the save returning within the 300 ms exercise target on your measured local run, then restart the API before dispatch and recover the pending job. Deliver the pending and ready responses plus the new worker start command.
Revised flow. These are proposed components to implement, not extra services started by the supplied demo.