Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -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.