Tasteprint architecture
Decision
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.
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.
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.
Runtime view
flowchart LR
U[Browser] --> N[Nginx]
N --> R[React application]
N --> A[Spring Boot API]
A --> P[(PostgreSQL)]
A --> M[(Photo volume)]
A --> H[Health and OpenAPI]
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.
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.
Backend modules
| Module | Owns | Depends on |
|---|---|---|
account |
Users, password hashes, session tokens, profile privacy | shared |
catalog |
Destinations, dishes, categories, cultural importance | shared |
tasting |
Tasting journal and ownership rules | account, catalog, media, shared |
journey |
Trips and persisted five-bite missions | catalog, tasting, shared |
progress |
Weighted coverage, important gaps, dashboard projection | account, catalog, tasting, journey |
social |
Public Tasteprints, comparisons, collaborative challenges | account, catalog, tasting, progress, shared |
media |
Validated local photo storage and public file route | account, shared |
demo |
Optional local sample accounts and activity | Application modules through public services |
shared |
Clock, API errors, OpenAPI configuration | None |
Repositories and JPA entities are package-private where another module does not need them. Modules communicate through public service methods and immutable record views.
Main flows
Record a tasting
Authenticated request
-> validate dish, date, rating, country, coordinates, and photo URL
-> verify the catalog dish exists
-> save a user-owned tasting
-> publish TastingRecorded
-> invalidate frontend queries
-> recalculate projections when they are read
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.
Build a trip mission
Create trip
-> load destination dishes
-> sort untried dishes before tried dishes
-> prefer higher cultural importance
-> select distinct categories first
-> persist five mission items
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.
Update a group challenge
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.
Data and consistency
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.
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.
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.
Security and privacy
- Passwords use BCrypt with cost 12.
- Session tokens contain 256 random bits, expire after 30 days, and are stored only as hashes.
- The API is stateless and protected by bearer authentication.
- Public profiles are opt-in and return a DTO without email or private account fields.
- Public tasting DTOs omit restaurant details, exact coordinates, and internal timestamps.
- User-owned tasting and trip lookups include the authenticated user ID.
- Challenge reads require membership. Destructive actions distinguish owner and member permissions.
- CORS is limited to configured origins.
- Photo upload accepts JPEG, PNG, or WebP, checks magic bytes, generates filenames, records ownership, rejects cross-account reuse, and prevents path traversal.
- Nginx blocks framing, disables MIME sniffing, sets a strict referrer policy, limits upload size, and rate limits login and registration in the supplied deployment.
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.
API design
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.
OpenAPI is available at /docs in development and can be disabled through Springdoc configuration in a restricted production environment.
Extraction criteria
A module should become a separate service only after evidence shows an independent need. Likely candidates are:
- Media, when object processing or moderation needs its own workers.
- Progress, when precomputed projections require independent scaling.
- Notifications, when challenge activity adds asynchronous email or push delivery.
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.