profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
ARCHITECTURE.md 6,895 bytes
1 # Tasteprint architecture
2
3 ## Decision
4
5 Tasteprint starts as a modular monolith because its current domains share consistency rules and its expected traffic does not justify independent infrastructure. A tasting updates the journal, trip mission, personal progress, comparison data, and challenge progress. Keeping one transactional database makes those results immediate and predictable.
6
7 The code still has explicit module boundaries. Spring Modulith and an architecture test prevent accidental package coupling. Each module owns its entities, repositories, service logic, and HTTP entry points.
8
9 Microservices would add network failure modes, distributed tracing, message delivery, separate migrations, and eventual consistency before the product has a scaling need. The current design keeps an extraction path without paying that cost now.
10
11 ## Runtime view
12
13 ```mermaid
14 flowchart LR
15 U[Browser] --> N[Nginx]
16 N --> R[React application]
17 N --> A[Spring Boot API]
18 A --> P[(PostgreSQL)]
19 A --> M[(Photo volume)]
20 A --> H[Health and OpenAPI]
21 ```
22
23 Nginx serves immutable frontend assets, applies a request-size limit and basic authentication rate limiting, proxies API requests, and returns the React entry page for client routes.
24
25 The backend is stateless at the HTTP layer. A session is a random opaque token. Only its SHA-256 hash is stored, so a database read does not reveal usable sessions.
26
27 ## Backend modules
28
29 | Module | Owns | Depends on |
30 | --- | --- | --- |
31 | `account` | Users, password hashes, session tokens, profile privacy | `shared` |
32 | `catalog` | Destinations, dishes, categories, cultural importance | `shared` |
33 | `tasting` | Tasting journal and ownership rules | `account`, `catalog`, `media`, `shared` |
34 | `journey` | Trips and persisted five-bite missions | `catalog`, `tasting`, `shared` |
35 | `progress` | Weighted coverage, important gaps, dashboard projection | `account`, `catalog`, `tasting`, `journey` |
36 | `social` | Public Tasteprints, comparisons, collaborative challenges | `account`, `catalog`, `tasting`, `progress`, `shared` |
37 | `media` | Validated local photo storage and public file route | `account`, `shared` |
38 | `demo` | Optional local sample accounts and activity | Application modules through public services |
39 | `shared` | Clock, API errors, OpenAPI configuration | None |
40
41 Repositories and JPA entities are package-private where another module does not need them. Modules communicate through public service methods and immutable record views.
42
43 ## Main flows
44
45 ### Record a tasting
46
47 ```text
48 Authenticated request
49 -> validate dish, date, rating, country, coordinates, and photo URL
50 -> verify the catalog dish exists
51 -> save a user-owned tasting
52 -> publish TastingRecorded
53 -> invalidate frontend queries
54 -> recalculate projections when they are read
55 ```
56
57 Progress is currently calculated on read. The catalog has 72 dishes and this keeps writes simple. If the dataset or traffic grows, the `TastingRecorded` event is the boundary for introducing cached projections without changing the API.
58
59 ### Build a trip mission
60
61 ```text
62 Create trip
63 -> load destination dishes
64 -> sort untried dishes before tried dishes
65 -> prefer higher cultural importance
66 -> select distinct categories first
67 -> persist five mission items
68 ```
69
70 A trip mission does not change after creation. This gives the user a stable plan. Completion requires both a date inside the trip and a tasting country equal to the destination code.
71
72 ### Update a group challenge
73
74 Challenge scores are derived from all participant tastings within the challenge dates. Dish slugs are restricted to the challenge destination before weighted coverage is calculated. A member can leave. The owner can delete the challenge but cannot leave an ownerless record.
75
76 ## Data and consistency
77
78 Flyway owns the schema and curated catalog. Hibernate uses `validate`, never automatic schema creation or mutation. Foreign keys and check constraints protect ownership relationships, date ordering, ratings, coordinates, category uniqueness, mission position, and challenge membership.
79
80 H2 in PostgreSQL compatibility mode keeps local startup and most tests fast. A Testcontainers test starts PostgreSQL when Docker is available to verify migrations and the core write flow against the production database engine.
81
82 Photo bytes are deliberately outside the relational database. The database stores ownership and file metadata, a tasting stores a relative upload URL, and a mounted volume stores bytes. `MediaStorage` is an interface so object storage can replace local disk without changing tasting logic. Removing a tasting removes its local photo. Account deletion removes unattached uploads as well.
83
84 ## Security and privacy
85
86 - Passwords use BCrypt with cost 12.
87 - Session tokens contain 256 random bits, expire after 30 days, and are stored only as hashes.
88 - The API is stateless and protected by bearer authentication.
89 - Public profiles are opt-in and return a DTO without email or private account fields.
90 - Public tasting DTOs omit restaurant details, exact coordinates, and internal timestamps.
91 - User-owned tasting and trip lookups include the authenticated user ID.
92 - Challenge reads require membership. Destructive actions distinguish owner and member permissions.
93 - CORS is limited to configured origins.
94 - Photo upload accepts JPEG, PNG, or WebP, checks magic bytes, generates filenames, records ownership, rejects cross-account reuse, and prevents path traversal.
95 - Nginx blocks framing, disables MIME sniffing, sets a strict referrer policy, limits upload size, and rate limits login and registration in the supplied deployment.
96
97 For a larger public deployment, put TLS and a managed rate limiter at the edge, move photos to object storage, rotate database credentials through a secret manager, and add automated backups.
98
99 ## API design
100
101 The API is versioned under `/api/v1`. Validation and domain errors use RFC 9457 problem details. Successful create requests return `201`, deletes and logout return `204`, private missing resources return `404` where revealing ownership would leak information, and permission failures return `403`.
102
103 OpenAPI is available at `/docs` in development and can be disabled through Springdoc configuration in a restricted production environment.
104
105 ## Extraction criteria
106
107 A module should become a separate service only after evidence shows an independent need. Likely candidates are:
108
109 1. Media, when object processing or moderation needs its own workers.
110 2. Progress, when precomputed projections require independent scaling.
111 3. Notifications, when challenge activity adds asynchronous email or push delivery.
112
113 Extraction would use the existing public service and domain event boundaries. Account, catalog, tasting, and journey should remain together until independent scaling or ownership becomes more valuable than transactional simplicity.
114