diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..6444165 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,106 @@ +# 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:** 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 + +**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 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 +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 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 +truth for server data. Reload, back/forward and shared links reproduce the same +search.