From 91cae3acd5f39b19f8657623295bf458be5f2144 Mon Sep 17 00:00:00 2001 From: Even Date: Mon, 28 Sep 2026 20:15:13 +0200 Subject: [PATCH 1/2] docs: #3 architecture decision records Add docs/architecture.md with six short decision records so peer reviewers and examiners can follow our reasoning. Each one covers context, options considered, the decision and its consequences: - PostgreSQL + GraphQL Yoga over MongoDB, Neo4j or a BaaS - keyset (cursor) pagination instead of OFFSET - sorting and filtering in SQL - per-request DataLoaders against N+1 - simulated users instead of login - plain CSS, Apollo Client cache and URL as search state Closes #3 --- docs/architecture.md | 103 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 docs/architecture.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..4fe2ebb --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,103 @@ +# Architecture decisions + +The main technical choices for Crate and why we made them. Most are implemented +in M2 and M3. The API contract is `schema.graphql`, and the data model is in +`docs/data-model.md`. + +## 1. PostgreSQL + GraphQL Yoga + +**Context:** The data is relational. Albums have many artists and genres, and +reviews and crate items link a user to an album. Search needs counts, genre +facets and average ratings. Everything runs on the group VM. + +**Options:** PostgreSQL, MongoDB, Neo4j, or a backend-as-a-service such as +Firebase. + +**Decision:** PostgreSQL with a GraphQL Yoga server. MongoDB would need +duplicated data or manual joins. Neo4j is built for graph traversal, and our +queries are search, filter, sort and page. A BaaS would hide the server-side +parts this project is about (decisions 2 to 4) and keep the data off our VM. + +**Consequences:** Joins, aggregates, constraints (one review per user per album) +and full-text search come built in. In return we write migrations and run the +database ourselves. + +## 2. Keyset pagination + +**Context:** The data set has no fixed size, so paging must stay fast on any +page. + +**Options:** `LIMIT`/`OFFSET`, or keyset pagination. + +**Decision:** Keyset. The cursor stores the last row's sort value and id, and +the next page continues from there (`first` + `after` in the schema). `OFFSET` +reads and throws away every skipped row, so it gets slower on later pages, and +it can repeat or skip rows when data changes between requests. + +**Consequences:** Every page is equally fast. Paging only goes forward, so there +is no "jump to page N". Each sort mode needs its own cursor logic, including +nullable fields such as release year. + +## 3. Sorting and filtering in SQL + +**Context:** Sorting and filtering must apply to the whole result set, not just +the loaded page. `totalCount` and genre facets also need every match. + +**Options:** In SQL, in the resolver, or in the client. + +**Decision:** In SQL, as one parameterised query built from the search term, +genres and sort. Sorting a loaded page in the client gives the wrong answer, and +sorting in the resolver loads every match into memory. + +**Consequences:** Results and counts are correct and can use indexes. The query +builder is the most complex server code, so it needs tests against a real +database. + +## 4. DataLoaders against N+1 + +**Context:** GraphQL resolves each field on its own. A page of 20 albums asking +for artists, genres and ratings would otherwise run a few queries per album. + +**Options:** Naive field resolvers, one large join up front, or DataLoaders. + +**Decision:** Every relation field goes through a DataLoader created per +request. The loader collects all album ids in the request and runs one query for +them. + +**Consequences:** A page costs a fixed number of queries whatever its size. +Because the loaders are per request, one user's data (`myReview`, `isInCrate`) +is never cached for another. + +## 5. Simulated users instead of login + +**Context:** Reviews and crates must persist on the server. The course allows +simulated users. + +**Options:** Real login, third-party login, or a simulated user. + +**Decision:** The client generates a UUID, keeps it in `localStorage` and sends +it with each request. The server reads it, so the `my...` fields in the schema +need no user argument. + +**Consequences:** Much less to build. There is no security, since anyone can +send another user's id, and clearing storage gives a new user. + +## 6. Plain CSS, Apollo Client cache, URL as search state + +**Context:** A small client that needs good accessibility, a small bundle, and +searches that survive reload and can be shared. + +**Options:** A CSS framework or component library, or plain CSS. A separate +state store such as Redux, or the Apollo cache. Search state in components, or +in the URL. + +**Decision:** + +- **Plain CSS** for a smaller bundle and full control over focus and contrast. +- **The Apollo cache** as the store for server data. Mutations return the + updated album, so every view of that album updates without a refetch. +- **The URL** holds the search term, genres and sort. + +**Consequences:** We write all the styling ourselves. There is one source of +truth for server data. Reload, back/forward and shared links reproduce the same +search. From 2fad6d42703904206b118394815a3ef414f7a2b3 Mon Sep 17 00:00:00 2001 From: Even Date: Wed, 30 Sep 2026 21:47:38 +0200 Subject: [PATCH 2/2] docs: docs: #3 address architecture review comments Reword three decisions after review: - keyset pagination: "paging stays efficient on later pages" instead of "every page is equally fast" - sorting and filtering: parameterised SQL queries, since results, totalCount and facets may be separate queries - Apollo cache: returning the album updates its cached data, but lists such as myCrate and reviews may still need a cache update or refetch Refs: #3 --- docs/architecture.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 4fe2ebb..6444165 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -34,9 +34,9 @@ the next page continues from there (`first` + `after` in the schema). `OFFSET` reads and throws away every skipped row, so it gets slower on later pages, and it can repeat or skip rows when data changes between requests. -**Consequences:** Every page is equally fast. Paging only goes forward, so there -is no "jump to page N". Each sort mode needs its own cursor logic, including -nullable fields such as release year. +**Consequences:** Paging stays efficient on later pages. Paging only goes +forward, so there is no "jump to page N". Each sort mode needs its own cursor +logic, including nullable fields such as release year. ## 3. Sorting and filtering in SQL @@ -45,9 +45,10 @@ the loaded page. `totalCount` and genre facets also need every match. **Options:** In SQL, in the resolver, or in the client. -**Decision:** In SQL, as one parameterised query built from the search term, -genres and sort. Sorting a loaded page in the client gives the wrong answer, and -sorting in the resolver loads every match into memory. +**Decision:** In SQL, as parameterised SQL queries built from the search term, +genres and sort. Results, `totalCount` and facets may be separate queries. +Sorting a loaded page in the client gives the wrong answer, and sorting in the +resolver loads every match into memory. **Consequences:** Results and counts are correct and can use indexes. The query builder is the most complex server code, so it needs tests against a real @@ -95,7 +96,9 @@ in the URL. - **Plain CSS** for a smaller bundle and full control over focus and contrast. - **The Apollo cache** as the store for server data. Mutations return the - updated album, so every view of that album updates without a refetch. + updated album, so Apollo updates that album's cached data without a refetch. + Related lists such as `myCrate` and `reviews` may still need a cache update or + refetch. - **The URL** holds the search term, genres and sort. **Consequences:** We write all the styling ourselves. There is one source of