Database Schema
MongoDB is document-oriented and schemaless at the database level, but every collection has a
concrete, enforced-in-code shape via Spring Data MongoDB @Document entities. This page is that
schema, kept in sync with services/spring/{user,content}-service/.../model/. See also the
Analysis Object Model diagram — a
conceptual, analysis-level model of the original problem domain (broader than what's implemented
so far) — and the Current Object Model, a UML
class diagram of exactly the collections documented below. This page is the field-level detail
underneath that current-state diagram.
Databases
Per the project's CLAUDE.md, user-service and content-service each own a separate MongoDB
logical database on the shared mongo:8 instance (infra/docker-compose.yaml) — no service
reads another service's collections directly. Cross-service references are plain string
ids resolved over REST, not native Mongo references (no @DBRef is used anywhere in either
service).
user-service database
users collection
Backed by User.java (rolling_restarts.user.model).
| Field | Type | Index | Notes |
|---|---|---|---|
_id |
String |
primary key (@Id) |
Mongo ObjectId string |
username |
String |
unique (@Indexed(unique = true)) |
|
email |
String |
unique (@Indexed(unique = true)) |
|
passwordHash |
String |
— | Hashed, never plaintext |
name |
String |
— | Display name |
avatarInitials |
String |
— | Derived initials for the avatar UI |
createdAt |
Instant |
— | Defaults to creation time |
settings |
UserSettings (embedded) |
— | See below |
Embedded: UserSettings (not a separate collection — nested inline in users)
| Field | Type | Notes |
|---|---|---|
selectedTopicIds |
List<String> |
References content-service's topics._id |
enabledSourceIds |
List<String> |
References content-service's sources._id |
savedArticleIds |
List<String> |
References content-service's articles._id |
Repository: UserRepository — derived queries only, no @Query annotations:
findByUsername, findByEmail, existsByUsername, existsByEmail (all leverage the unique
indexes above).
Known gap: unlike
content-service(below),user-service'sapplication.propertiesdoes not setspring.data.mongodb.auto-index-creation, and nothing else creates these indexes manually (no init script, noMongoTemplateindex setup). Spring Data MongoDB's auto-index-creation defaults tofalse, so on a fresh deployment theusername/existsByUsername/existsByEmailchecks at the application level, not necessarily by a real unique index at the database level. Confirmed by readingapplication.propertiesdirectly — not yet fixed in code.
content-service database
topics collection
Backed by Topic.java (rolling_restarts.content.model).
| Field | Type | Index | Notes |
|---|---|---|---|
_id |
String |
primary key (@Id) |
|
name |
String |
— | |
color |
String |
— | UI accent color |
Repository: TopicRepository — no custom query methods.
sources collection
Backed by Source.java (rolling_restarts.content.model).
| Field | Type | Index | Notes |
|---|---|---|---|
_id |
String |
primary key (@Id) |
Deterministic, not a random ObjectId — a UUIDv3 derived from the normalized RSS URL (Source.idForRssUrl()), so re-adding a previously-deleted feed reuses the same id and keeps its old articles associated |
name |
String |
— | |
rssUrl |
String |
unique (@Indexed(unique = true)) |
|
initials |
String |
— | |
active |
boolean |
— | Default true |
subscriberCount |
int |
— | Default 0; source is auto-removed when this hits 0 |
lastFetchedAt |
Instant |
— | |
fetchStatus |
FetchStatus (embedded enum) |
— | PENDING | SUCCESS | FAILED, default PENDING |
fetchError |
String |
— | Short error message when fetchStatus = FAILED |
createdAt |
Instant |
— | Defaults to creation time |
Repository: SourceRepository — findByRssUrl, findByActiveTrue, existsByRssUrl (all
derived, no @Query).
articles collection
Backed by Article.java (rolling_restarts.content.model).
| Field | Type | Index | Notes |
|---|---|---|---|
_id |
String |
primary key (@Id) |
|
headline |
String |
text (@TextIndexed) |
Part of the combined text index with snippet |
snippet |
String |
text (@TextIndexed) |
|
imageUrl |
String |
— | |
body |
List<String> |
— | Article body paragraphs |
sourceId |
String |
@Indexed |
References sources._id |
topicId |
String |
@Indexed |
References topics._id |
author |
String |
— | |
publishedAt |
Instant |
— | |
readingMinutes |
int |
— | |
fetchedAt |
Instant |
— | Defaults to fetch time |
externalUrl |
String |
unique (@Indexed(unique = true)) |
Dedupe key — prevents re-ingesting the same article twice |
Repository: ArticleRepository — findByIdIn, existsByExternalUrl, findByExternalUrlIn
(all derived, no @Query).
Text search: ArticleService (rolling_restarts.content.service) issues the ?q= search
directly via MongoTemplate/TextQuery/TextCriteria (not a repository method), matching
against the combined headline+snippet text index above, further filtered by sourceId/
topicId and paginated.
content-service's application.properties explicitly sets
spring.data.mongodb.auto-index-creation=true so this text index (and the other @Indexed
fields on this page) actually get created on startup — Spring Data MongoDB disables index
auto-creation by default, and without this the text search fails outright with
MongoCommandException: text index required for $text query (IndexNotFound).
Cross-collection relationship summary
users.settings.selectedTopicIds ──▶ topics._id (content-service)
users.settings.enabledSourceIds ──▶ sources._id (content-service)
users.settings.savedArticleIds ──▶ articles._id (content-service)
articles.sourceId ──▶ sources._id (content-service, same DB)
articles.topicId ──▶ topics._id (content-service, same DB)
None of these are enforced at the database level (no @DBRef, no foreign keys — MongoDB doesn't
support cross-document referential integrity natively); they're plain string ids resolved by the
owning service's REST API at read time.