Skip to content

Latest commit

 

History

History
993 lines (758 loc) · 55.1 KB

File metadata and controls

993 lines (758 loc) · 55.1 KB

StreamSync - Technical Report

GitHub Repository: https://github.com/serc-project/stream-sync


Table of Contents

  1. Project Overview
  2. Requirements
  3. Stakeholder Identification
  4. Architecture Design
  5. Architectural Tactics
  6. Subsystem Overview
  7. Implementation Patterns
  8. Architecture Analysis
  9. Individual Contributions

1. Project Overview

StreamSync is a microservices-based movie recommendation and social tracking platform. It solves the fragmentation problem of modern streaming: users subscribe to many OTT services but have no single place to track what they have watched, discover what to watch next, or coordinate viewing with friends.

The system combines:

  • A unified media tracker that logs ratings and watch history across services.
  • A sequence-aware ML recommendation engine powered by a State Space Model (SSM/Mamba) that evolves a compact per-user latent vector over their chronological watch history.
  • A semantic "vibe" search interface backed by sentence-transformer embeddings and FAISS approximate-nearest-neighbour retrieval.
  • A Watch Party Blend feature that combines multiple users' SSM states into a group consensus recommendation, filtering out titles already seen by any participant.
  • A social layer (follows, friend feeds, public profiles) for taste comparison and group coordination.

The runtime is fully containerised. Seven independently deployable units are orchestrated via Docker Compose: a React SPA, a Spring Cloud Gateway, four Spring Boot Java microservices, and one Python FastAPI ML microservice.


2. Requirements

2.1 Functional Requirements

# Requirement Evidence in Codebase
FR-1 Unified Media Tracker — Users must be able to log, update, and remove watched movies with optional 0–10 ratings, and maintain a separate plan-to-watch (watchlist). UserProfileControllerPOST /watched-movies, PUT /watched-movies/{id}, DELETE /watched-movies/{id}, POST /watchlist/{tmdbId}
FR-2 Sequence-Aware Recommendations — The system must generate personalised movie recommendations by continuously evolving a per-user SSM state vector that encodes the chronological sequence of their watch history. UserSsmStateService.updateUserStateFromWatchedMovie() calls POST /update_state; getRecommendedMovieIds() calls POST /search-by-state on the ssm-engine.
FR-3 Semantic "Vibe" Search — Users must be able to query movies using free-form natural language (e.g., "claustrophobic sci-fi with a twist") instead of exact titles or genres. POST /semantic-search in app.py; POST /api/movies/semantic-search in MovieController. Fallback to SQL LIKE search if ssm-engine is unavailable.
FR-4 Watch Party / Blend — The system must accept a list of ≥ 2 usernames, combine their individual SSM states into a group preference, and return a consensus recommendation list that excludes movies all participants have already seen. WatchPartyService.getWatchPartyRecommendations()POST /combine-states on ssm-engine.
FR-5 Social Layer — Users must be able to follow others, view mutual follow (friend) relationships, and query any public profile's watch history. UserProfileControllerPOST /follow/{friendName}, POST /unfollow/{friendName}, GET /followers, GET /{username} with public/private visibility gate.
FR-6 Movie Catalogue — The system must maintain a searchable Postgres catalogue of movies (title, genres, cast, director, ratings, poster) sourced from the Kaggle/TMDB dataset. MovieCatalogBootstrapLoader streams NDJSON from ssm-engine /movie-database-stream and bulk-inserts into Postgres via batchUpdate.
FR-7 Scheduled Catalogue Refresh — The movie catalogue must refresh automatically every 24 hours so it stays in sync with the latest TMDB snapshot. @Scheduled(fixedDelayString = "${movie.catalog.refresh.interval-ms:86400000}") in MovieCatalogBootstrapLoader.scheduledRefresh().
FR-8 SSM State Persistence — Per-user SSM state vectors must survive service restarts by being persisted to a dedicated Postgres table. user_ssm_state table with UPSERT in UserSsmStateService; Docker volume ssm-data for FAISS artifacts.
FR-9 Authentication & Access Control — All user-mutation endpoints must verify a Bearer JWT token, and data access must be scoped to the token owner. extractUsername() in UserProfileController; isOwnedRequest() guard on every write endpoint; auth-service issues tokens via /api/auth/**.
FR-10 Movie Detail Enrichment — Single-movie and batch-movie metadata must be retrievable for display, with a SQL fallback when the ML engine is down. MovieQueryService.findByTmdbId(), findByTmdbIds(), findMostPopular(), findHighestRated(), search().

2.2 Non-Functional Requirements

# Requirement Metric / Target Architectural Significance
NFR-1 Low-Latency ML Inference < 300 ms for core recommendation and search queries Prevents the use of full-context transformers at query time. Enforces the use of the Mamba/S4D State Space Model (linear recurrence, O(L) not O(L²)) and FAISS IndexFlatIP for sub-millisecond ANN search. The SSM state is maintained as a compact floating-point tensor stored in Postgres; only a single step() forward pass is needed per new watch event.
NFR-2 Decoupled Scalability ML workloads must scale independently of CRUD operations Mandates a microservices architecture with strict service boundaries. Java Spring Boot containers handle transactional user/movie CRUD. Python FastAPI (ssm-engine) handles all PyTorch inference and FAISS operations. They communicate only over HTTP REST, allowing each tier to be replicated or replaced independently.
NFR-3 Fault Tolerance / ML Fallback System must remain functional even if ssm-engine is unreachable Implements the Strategy Pattern in MovieController.semanticSearch(): if ssmEngineClient.semanticSearch() returns empty, the system transparently falls back to movieQueryService.search() (SQL LIKE). Similarly, UserSsmStateService catches exceptions from the ssm-engine to avoid propagating 5xx to the end user on watch-event updates.
NFR-4 Asynchronous, Non-Blocking Index Rebuild Rebuilding the ~900k-row FAISS index must not block API traffic The MovieCatalogBootstrapLoader uses Java's HttpClient streaming (HttpResponse.BodyHandlers.ofLines()) to consume NDJSON chunks, decoupled from the main request thread. The ssm-engine tracks _rebuild_in_progress state and exposes a /build-status endpoint; Docker volume ssm-data ensures the index persists across container restarts.
NFR-5 Data Isolation Each microservice owns its own persistent store Two separate Postgres instances (movie-db on port 5432, recommendation-db on port 5433) with independent schemas. User profiles are stored in a MongoDB-style document (JPA @Document) within the user-service. This prevents cross-service schema coupling and allows independent DB upgrades.
NFR-6 GPU Utilisation & Adaptive Batching GPU usage heavily spikes for ~30 minutes during daily FAISS indexing; inference uses minimal GPU compute build_index.py handles the daily heavy-lifting (the 30-minute semantic re-index of ~900k titles) using adaptive batch size reduction on CUDA OOM. Outside this indexing window, the PyTorch SSM forward passes (step()) and FAISS fast inner-product search consume very little GPU payload, making it possible to run on mid-tier hardware (e.g., RTX 2060).
NFR-7 Security (RBAC + JWT) No user may mutate another user's data Every write endpoint in UserProfileController calls isOwnedRequest() which validates the Authorization: Bearer <JWT> header against the path-variable username. Tokens are HMAC-SHA256 signed with a configurable secret. Auth service is a separately deployable container.
NFR-8 Recommendation Quality Recommendations must not be dominated by obscure or low-vote movies Quality-aware re-ranking in _quality_score() blends semantic cosine similarity (weight 0.50) with IMDB/TMDB rating (0.20), log-normalised TMDB popularity (0.15), and log-normalised vote count (0.15). Over-fetches 5× top_k candidates from FAISS, then re-ranks in memory.
NFR-9 High Availability 99.9% API uptime, maintaining operation during deep ETL processing Achieved through: (1) in-memory hot swapping of the FAISS index during rebuilds; (2) service decoupling so auth and user tracking are unaffected by ML latency; (3) Strategy fallbacks to SQL LIKE queries when the ML engine fails; and (4) non-destructive ON CONFLICT DO UPDATE upserts during scheduled TMDB catalogue streaming.

3. Stakeholder Identification

3.1 Stakeholders and Concerns

Stakeholder Role / Interest Concerns
Casual Viewers & Enthusiasts Primary end users seeking personalised discovery and persistent cross-platform history Unified watch history across OTT services; recommendations that evolve with long-range taste patterns; vibe-based natural-language search
Friend Groups & Families Groups coordinating shared viewing sessions Conflict-free group recommendation (Watch Party Blend); real-time OTT availability per title
Content Reviewers Users maintaining immutable rating logs and engaging socially Persistent review log; social features (follow, taste comparison, notifications)
Frontend Lead Owns React SPA, UI/UX, client-side integration Clear, stable REST API contracts between frontend and backend
Backend Lead Owns Spring Boot services, JWT auth, PostgreSQL schema Maintainability of service boundaries; correct domain model shared across services
ML / Data Lead Owns FastAPI service, Mamba SSM, vector embeddings, TMDB pipeline ML inference latency (< 300 ms); correct normalisation of TMDB metadata; reproducible sequential data format for Mamba training
Arch / Integration Lead Owns Blend algorithm, TMDB Adapter, inter-service communication, QA Independent deployability of ML vs. core services; graceful degradation when ML service is unavailable; testability of integration boundaries
System Architect Responsible for overall coherence and non-functional properties Security and data ownership; scalability; loose coupling between services

3.2 Viewpoint 1 — Functional Decomposition

mindmap
  root((StreamSync))
    Auth & Identity
      JWT issuance and validation
      Role-Based Access Control
    Unified Tracker
      Watch log CRUD
      OTT availability lookup via TMDB Adapter
    Recommendation Engine
      Primary: Mamba SSM via FastAPI <300ms
      Fallback: genre/rating filter Strategy pattern
    Semantic Search
      Natural-language query to vector embedding
      Cosine similarity over overviews, themes, tags
    Blend Engine
      Aggregate histories and PTW lists per group
      Filter already-seen titles per member
      Score candidates across group preferences
      Observer: notify members when Blend is ready
    Social Layer
      Follow graph and friend feed
      Taste profile comparison
      Blend invitations and notifications
Loading

3.3 Viewpoint 2 — Component & Connector (Runtime Architecture)

graph TD
    Browser["Browser\n(React SPA)"]

    subgraph Gateway["Spring Boot Gateway"]
        Auth["AuthController\nJWT issuance + RBAC"]
        Tracker["TrackerController\nWatchEntry CRUD"]
        Blend["BlendController\nWatchGroup + Blend lifecycle"]
        Social["SocialController\nFollow graph, friend feed"]
        Search["SearchController"]
    end

    subgraph ML["FastAPI ML Service"]
        Recommend["/recommend\nMamba SSM"]
        BlendAlgo["/blend\nBlend algorithm"]
        VectorSearch["/search\nVector search"]
    end

    PostgreSQL[("PostgreSQL\nusers, watch_entries,\nmedia_items, blends")]
    TMDB["TMDB API\nMetadata + OTT availability"]

    Browser -->|"HTTPS / REST\nJWT Bearer"| Gateway
    Search -->|REST| ML
    Gateway -->|Spring JPA| PostgreSQL
    Gateway -->|TMDB Adapter| TMDB
Loading
Pattern Where Why
Strategy Recommendation Engine Swaps Mamba SSM for a simpler filter at runtime if the ML service is down
Adapter TMDB integration Isolates the domain model from TMDB JSON schema changes; produces typed MediaItem / OTTAvailability objects
Observer Blend notifications Core Services publish a "Blend ready" event; group members are notified without polling
Microservices Spring Boot ↔ FastAPI Decouples transactional CRUD from AI compute; services are independently deployable and scalable

3.4 Viewpoint 3 — Information (Core Domain Model)

erDiagram
    USER {
        uuid id
        string username
        string email
    }
    WATCH_ENTRY {
        uuid id
        enum status
        int rating
        text review
    }
    MEDIA_ITEM {
        uuid id
        string tmdb_id
        string title
        text overview
        string genres
        string tags
        vector embedding
    }
    OTT_AVAILABILITY {
        uuid id
        string platform
        string deeplink
        string region
    }
    WATCH_GROUP {
        uuid id
        string name
    }
    BLEND {
        uuid id
        json candidate_list
        json scores
        enum status
    }
    SOCIAL_EDGE {
        uuid follower_id
        uuid followee_id
    }

    USER ||--o{ WATCH_ENTRY : "owns"
    WATCH_ENTRY }o--|| MEDIA_ITEM : "references"
    MEDIA_ITEM ||--o{ OTT_AVAILABILITY : "available on"
    USER }o--o{ WATCH_GROUP : "member of"
    WATCH_GROUP ||--o{ BLEND : "produces"
    USER ||--o{ SOCIAL_EDGE : "follows"
Loading

MediaItem and OTTAvailability are produced exclusively by the TMDB Adapter, ensuring the rest of the system never touches raw TMDB JSON. WatchEntry.status drives both the personalisation pipeline and Blend filtering.

3.5 Viewpoint 4 — Security (Auth and Authorisation Flow)

sequenceDiagram
    actor Client
    participant Filter as JWT Filter
    participant Auth as AuthController
    participant DB as PostgreSQL
    participant Ctrl as Resource Controller

    Client->>Auth: POST /auth/login {username, password}
    Auth->>DB: validate credentials
    DB-->>Auth: user record + role
    Auth-->>Client: signed JWT {user_id, role, expiry}

    Client->>Filter: GET /api/... Authorization: Bearer <token>
    Filter->>Filter: validate signature + expiry
    Filter->>Ctrl: request + SecurityContext {user_id, role}
    Ctrl->>Ctrl: @PreAuthorize check
    Note over Ctrl: WatchEntry: owner_id == principal_id
    Note over Ctrl: Blend: principal is WatchGroup member
    Ctrl-->>Client: 200 OK / 403 Forbidden
Loading
Control Mechanism
Authentication Signed, short-lived JWT
Authorisation RBAC via Spring Security @PreAuthorize
Data ownership Service-layer ownership check (owner_id == principal_id)
Transport HTTPS for all inter-service and external communication
Secret management JWT signing key and DB credentials via environment variables

4. Architecture Design

4.1 System Overview

StreamSync is deployed as 8 independently containerised units: a React SPA, an API Gateway, five Spring Boot Java services (auth, user, movie, recommendation, interaction), and one Python FastAPI ML service (ssm-engine). Services communicate over HTTP REST; the user-service additionally publishes internal Spring events consumed asynchronously by RecommendationStateNotifier.

C4Container
    title Implemented — Microservices Architecture
    Person(u, "User")
    System_Boundary(ss, "StreamSync") {
        Container(spa,  "React SPA",             "Vite + React",         "Browser UI")
        Container(gw,   "API Gateway",           "Spring Cloud Gateway", "Route + CORS")
        Container(auth, "auth-service",          "Spring Boot",          "JWT issuance")
        Container(usr,  "user-service",          "Spring Boot",          "Profiles, history, social")
        Container(mov,  "movie-service",         "Spring Boot",          "Catalog + search")
        Container(rec,  "recommendation-service","Spring Boot",          "SSM state + Blend")
        Container(ssm,  "ssm-engine",            "FastAPI + PyTorch",    "ML inference, FAISS")
        ContainerDb(mdb,"movie-db",              "PostgreSQL",           "Movie catalog")
        ContainerDb(rdb,"recommendation-db",     "PostgreSQL",           "SSM states + embeddings")
        ContainerDb(udb,"userdb",                "H2 (file)",            "User profiles")
        ContainerDb(adb,"authdb",                "H2 (file)",            "Credentials")
    }
    Rel(u,   spa,  "HTTPS")
    Rel(spa, gw,   "HTTPS + Bearer JWT")
    Rel(gw,  auth, "/api/auth/**")
    Rel(gw,  usr,  "/api/user/**")
    Rel(gw,  mov,  "/api/movies/**")
    Rel(gw,  rec,  "/api/recommendation/**")
    Rel(mov, ssm,  "REST — semantic search")
    Rel(rec, ssm,  "REST — SSM state / Blend")
    Rel(usr, rec,  "Async event — watched movie")
    Rel(mov, mdb,  "JDBC")
    Rel(rec, rdb,  "JDBC")
    Rel(usr, udb,  "JPA")
    Rel(auth,adb,  "JPA")
Loading

Key structural properties:

Property Value
Deployable units 8 containers (+ 2 Postgres instances + 1 H2 each for auth/user)
Inter-service communication HTTP REST (synchronous), Spring ApplicationEventPublisher (async in-process)
Technology boundary Java (5 services) / Python (1 service) split enforced at the wire
Datastore strategy Database-per-service (2 × Postgres, 2 × H2)
Java source (non-test) ~3,300 lines across 46 files, 5 services
Python source ~1,900 lines across 6 modules (app.py, ssm_model.py, build_index.py, adapters, strategies)
ML inference path hops 3 HTTP hops: Browser → Gateway → movie-servicessm-engine

5. Architectural Tactics

Tactic 1 — Service Decomposition behind an API Gateway (Microservices)

NFRs addressed: Modifiability, Deployability, Scalability, Independent Evolvability.

The system is broken into narrowly-scoped services — authentication, user profile management, the movie catalog, user interactions, recommendations, and an ML inference engine — all fronted by a single API Gateway that routes requests by path prefix.

Each service owns its own datastore (database-per-service) so that schema evolution in one domain never cascades into another. The ML inference service is entirely Python-based, isolated from the JVM services, so GPU dependencies, model updates, and Python library upgrades have no impact on the transactional Spring Boot core.

C4Container
    title StreamSync — Planned Container Layout
    Person(user, "Viewer / Friend Group")
    System_Boundary(ss, "StreamSync") {
        Container(spa, "React SPA", "Vite + React", "Single-page UI")
        Container(gw, "API Gateway", "Spring Cloud Gateway", "Path-routed entrypoint; JWT validation")
        Container(auth, "auth-service", "Spring Boot", "JWT issuance & credential management")
        Container(usr, "user-service", "Spring Boot", "Profiles, follows, watchlist, watch history")
        Container(mov, "movie-service", "Spring Boot", "Movie catalog reads & semantic search relay")
        Container(rec, "recommendation-service", "Spring Boot", "SSM state management & Watch Party blends")
        Container(int, "interaction-service", "Spring Boot", "Ratings and interaction events")
        Container(ssm, "ssm-engine", "FastAPI + PyTorch + FAISS", "Embedding inference, ANN search, group aggregation")
        ContainerDb(mdb, "movie-db", "Postgres", "Movie catalog")
        ContainerDb(rdb, "recommendation-db", "Postgres", "User SSM states & embeddings")
        ContainerDb(cache, "Redis", "Key-value store", "Blend cache & Pub/Sub (planned)")
    }
    Rel(user, spa, "Uses")
    Rel(spa, gw, "HTTPS + Bearer JWT")
    Rel(gw, auth, "/api/auth/**")
    Rel(gw, usr, "/api/user/**")
    Rel(gw, mov, "/api/movies/**")
    Rel(gw, rec, "/api/recommendation/**")
    Rel(mov, ssm, "REST — semantic search")
    Rel(rec, ssm, "REST — SSM state update / search / group blend")
    Rel(usr, rec, "Internal event — watched movie notification")
    Rel(mov, mdb, "JDBC")
    Rel(rec, rdb, "JDBC")
    Rel(rec, cache, "Cache reads/writes (planned)")
Loading

Future extension: The gateway layer is the natural point to plug in rate-limiting, circuit breakers, and API versioning without touching individual services.


Tactic 2 — Pluggable Algorithms with Graceful Degradation (Strategy + Fallback Chain)

NFRs addressed: Availability, Fault Tolerance, Latency, Maintainability.

Every algorithm-heavy request path is decomposed into an ordered chain of strategies:

  1. Primary — the richer, ML-backed path (semantic vector search, Mamba SSM recommendations, group state aggregation).
  2. Fallback — a simpler, self-contained alternative that has no dependency on the ML service (keyword/SQL-based search, static genre-based recommendations, pre-cached blend results).

Each strategy signals availability explicitly — returning an empty result rather than throwing — so the fallback chain activates silently without exposing error state to the caller.

flowchart TD
    A[Search Request] --> B{Semantic Search\nML Service available?}
    B -- Yes, hits found --> C[Return ranked vector results]
    B -- No / empty --> D{Keyword Search\nSQL LIKE fallback}
    D -- Results found --> E[Return keyword results]
    D -- Empty --> F[Return empty list]
Loading

Future extension: Wrapping each primary strategy call in a circuit breaker (e.g. Resilience4j) was planned as the production hardening step.


Tactic 3 — Adapter / Anti-Corruption Layer for External Integrations

NFRs addressed: Interoperability, Correctness, Modifiability, Data Integrity.

An Adapter is placed at every integration boundary:

  • TMDB Adapter: responsible for reading raw TMDB records in whatever format they arrive (CSV rows, API JSON) and producing clean, validated domain objects with sanitised numeric values.
  • SSM Engine Adapter: responsible for translating between the Java domain model (typed DTOs with named fields) and the ML service's HTTP/JSON protocol (flat maps, serialised tensor lists).
flowchart LR
    subgraph Java Domain
        A[RecommendationService]
        B[MovieCatalogService]
    end
    subgraph Adapters
        C[SsmEngineAdapter\ntyped DTO ↔ HTTP/JSON]
        D[TmdbAdapter\nraw CSV/API → domain objects]
    end
    subgraph External
        E[ssm-engine\nFastAPI REST]
        F[TMDB\nCSV / API]
    end
    A --> C --> E
    B --> D --> F
Loading

Future extension: Formalising the adapter boundary with an OpenAPI schema for the ML service contract, and introducing schema versioning so the Java layer could negotiate which API version to use.


Tactic 4 — Asynchronous Event-Driven State Propagation (Observer Pattern)

NFRs addressed: Responsiveness, Availability, Extensibility, Loose Coupling.

The event-driven decoupling works as follows:

  1. The user-service controller records the watch event and immediately returns a success response.
  2. An in-process event is published (WatchedMovieAddedEvent) carrying the username and movie identifier.
  3. A dedicated observer (RecommendationStateNotifier) picks up the event asynchronously on a background thread and forwards it to the recommendation service.
  4. Failure in the notification is non-fatal and logged as a warning — the user's action always succeeds.
sequenceDiagram
    participant U as User (Browser)
    participant UC as user-service
    participant EQ as Event Bus (in-process)
    participant RN as RecommendationStateNotifier
    participant RS as recommendation-service

    U->>UC: POST /watched {tmdb_id}
    UC->>UC: Persist watch record
    UC->>EQ: publish WatchedMovieAddedEvent
    UC-->>U: 200 OK (immediate)
    EQ-->>RN: onWatchedMovieAdded (async)
    RN->>RS: POST /internal/ssm-state/watched
    RS->>RS: Update Mamba SSM state
    note over RN: Failure here is non-fatal
Loading

Future extension: Replacing the in-process event bus with a durable message broker (Kafka or RabbitMQ) for cross-service fan-out, guaranteed at-least-once delivery, and dead-letter queue handling.


Tactic 5 — Offline Pre-computation with Streaming Ingestion and Health-Gated Coordination

NFRs addressed: Performance (< 300 ms latency), Scalability, Data Freshness, Reliability.

The expensive work is done before any user request arrives:

  • At startup (or on a scheduled 24h refresh), the ML service downloads the latest TMDB dataset and builds a FAISS approximate-nearest-neighbour index from pre-computed embeddings.
  • At query time, only the user's free-text query needs to be embedded — a single short string.
  • The same in-memory index serves semantic search, Mamba SSM state-based recommendations, and Watch Party group blends.

Ingestion is implemented as a streaming pipeline: the ML service exposes NDJSON-streaming endpoints, and downstream Java services consume them in bounded chunks via line-by-line HTTP streaming, writing to their own databases in small batches. This caps memory usage regardless of catalog size.

sequenceDiagram
    participant K as Kaggle (dataset source)
    participant SSM as ssm-engine
    participant MS as movie-service
    participant RS as recommendation-service

    SSM->>K: Download latest TMDB CSV
    SSM->>SSM: Build FAISS index + generate embeddings
    SSM->>SSM: /health → "ready"
    MS->>SSM: GET /movie-database-stream (NDJSON chunks)
    SSM-->>MS: stream chunk 1 … chunk N
    MS->>MS: Upsert catalog rows (bounded JDBC batches)
    RS->>SSM: Poll /build-status until stream_finished
    RS->>SSM: GET /rebuild-and-embeddings-stream
    SSM-->>RS: stream embedding vectors (NDJSON)
    RS->>RS: Persist embeddings to recommendation-db
    note over MS,RS: Both consumers are memory-bounded\nregardless of catalog size
Loading

6. Subsystem Overview

StreamSync is decomposed into seven discrete subsystems, each containerised and independently deployable.

React SPA (5173)
    │  HTTP REST
    ▼
API Gateway (8080)
    │  routes to
    ├─► Auth Service
    ├─► User Service
    ├─► Movie Service  ──────────────┐
    |                                │ HTTP REST
    └─► Recommendation Service ──────┤
                                     ▼
                               SSM Engine (5000)
                                     │
                         ┌───────────┴───────────┐
                    FAISS Index             Kaggle/TMDB CSV
                    (ssm-data volume)

6.1 API Gateway (services/api-gateway)

Technology: Java Spring Boot | Exposed Port: 8080

Single entry point for all inbound client traffic. It routes requests to the appropriate downstream microservice, acting as a simple reverse-proxy/router that keeps the client decoupled from the internal service topology. All cross-cutting concerns (rate limiting, request logging) are centralised here.


6.2 Auth Service (services/auth-service)

Technology: Java Spring Boot
Endpoints: POST /api/auth/register, POST /api/auth/login

Handles user registration and password-based authentication. On successful login it issues HMAC-SHA256 signed JWTs that are validated locally by downstream services without any further round-trip to this service. The stateless token design means the auth service is only hit at login/register time, keeping it lightweight and independently scalable.


6.3 User Service (services/user-service)

Technology: Java Spring Boot + JPA
Endpoints: GET|PUT /{username}/settings, POST|DELETE /{username}/watchlist/{id}, POST|GET|PUT|DELETE /{username}/watched-movies, POST /{username}/follow/{friend}, GET /{username}/followers

The central user state microservice. It owns the UserProfile document which stores the watchlist, full watched-movie history (with timestamps and ratings), and social relationships (UserRelationship). Every POST /watched-movies triggers a fire-and-forget async call to the Recommendation Service's internal SSM state update endpoint, creating the event-driven bridge that keeps the ML model in sync with user activity without blocking the CRUD response.


6.4 Movie Service (services/movie-service)

Technology: Java Spring Boot + PostgreSQL (movie-db)
Endpoints: GET /api/movies/{id}, POST /api/movies/batch, GET /api/movies/popular, GET /api/movies/top-rated, GET /api/movies/search, POST /api/movies/semantic-search

Manages the TMDB movie catalogue persisted in Postgres. On startup (or scheduled refresh every 24h), MovieCatalogBootstrapLoader streams the full Kaggle dataset from the ssm-engine as NDJSON and bulk-inserts it via JDBC batch updates. For semantic search, MovieController acts as an Adapter: it forwards the query to SsmEngineClient.semanticSearch(), receives FAISS hit IDs, enriches them with full MovieDto metadata from Postgres (preserving score order), and falls back to a SQL LIKE search if the ML engine is unavailable.


6.5 Recommendation Service (services/recommendation-service)

Technology: Java Spring Boot + PostgreSQL (recommendation-db)
Endpoints: GET /api/recommendation/users/{username}/movie-ids, POST /api/recommendation/watch-party, POST /internal/ssm-state/watched

The broker between the Java backend and the Python ML engine. It has two roles:

  1. SSM State Lifecycle (UserSsmStateService): When notified of a new watch event, it retrieves the user's last serialised SSM state from Postgres, POSTs it with the new tmdb_id to /update_state on the ssm-engine, and UPSERTs the returned new state back to user_ssm_state. For recommendation retrieval it POSTs the stored state to /search-by-state.

  2. Watch Party Orchestration (WatchPartyService): Reads persisted SSM states for all listed participants, builds a JSON payload with all states and exclusion lists, and POSTs to /combine-states on the ssm-engine. Returns the resolved movie_ids list plus metadata about which users had available states.


6.6 SSM Engine (ssm-engine/)

Technology: Python FastAPI + PyTorch (S4D SSM) + FAISS + SentenceTransformers
Exposed Port: 5000 | GPU: Optional NVIDIA GPU

The core ML microservice — implements every computation-intensive operation:

Endpoint Purpose
POST /update_state Runs one SSM step() forward pass with a movie embedding; returns updated serialised state
POST /search Rebuilds full SSM state from an ordered ID list; FAISS ANN search + quality re-rank
POST /search-by-state Deserialises stored state, applies query fusion (α × E_newest + (1−α) × S_ssm), FAISS ANN + re-rank
POST /semantic-search Encodes a free-text query with all-MiniLM-L6-v2; FAISS ANN + re-rank
POST /combine-states Watch Party: per-user individual FAISS retrieval → Least Misery (min similarity) post-retrieval re-ranking
GET /movie-database-stream Streams the Kaggle TMDB CSV as chunked NDJSON
GET /rebuild-and-embeddings-stream Triggers a clean FAISS index rebuild then streams all embeddings
GET /health, GET /build-status Operational status and indexing progress

The SSM model (ssm_model.py) is a MultiScaleS4D: a stack of S4DBlock layers each containing an S4DKernel (ZOH-discretised diagonal state-space model with complex-valued A). The readout uses recency-weighted pooling blended with a deterministic fallback for stability without a trained checkpoint.

The FAISS index (build_index.py) uses IndexFlatIP on L2-normalised all-MiniLM-L6-v2 embeddings of rich text documents (Title · Genres · Director · Cast · Tagline · Synopsis · Decade). GPU-adaptive batch embedding with automatic CUDA OOM recovery ensures robust builds on constrained hardware.


6.7 Frontend (frontend/)

Technology: React.js + Vite + Tailwind CSS | Exposed Port: 5173

Single-Page Application providing the end-user interaction layer. It is structured around eight page-level modules:

Page Purpose
auth Login / registration forms
home Landing / discovery feed
moviePage Single-movie detail view
profile User profile, watch history, social stats
accountPage Account settings
recommendations Personalised SSM recommendation list
searchPage Semantic "vibe" search interface
socialPage Friends, followers, Watch Party Blend launcher

Shared application state (auth token, current user) is managed via React Context (context/). API calls to the backend are centralised in the services/ layer. The app is hot-reload capable in development via Docker volume bind-mounts (./frontend/src:/app/src).


7. Implementation Patterns

7.1 Pattern 1 — Strategy (with Composite Fallback Chain)

Problem

StreamSync's movie search must serve a natural-language "vibe" query against a corpus of hundreds of thousands of titles. The ideal path is semantic vector search via the ssm-engine ML service. However, the ML service is an external dependency with its own startup time, GPU requirements, and failure modes. Hard-coding the call to ssm-engine would propagate any ML service unavailability directly to the user as an error.

The same tension exists in Watch Party group scoring: the product wants to experiment with different aggregation methods (least-misery, average, multiplicative) without restructuring the endpoint each time.

The Strategy pattern solves both problems by defining a common algorithm interface and allowing concrete implementations to be swapped at configuration time (search) or at call time (group aggregation) — without changing any calling code.

Participants (movie-service search layer)

Role Class Package
Strategy interface MovieSearchStrategy movie_service.search
Concrete Strategy A SemanticSearchStrategy movie_service.search
Concrete Strategy B KeywordSearchStrategy movie_service.search
Composite Strategy FallbackSearchStrategy movie_service.search
Context MovieController movie_service
Composer / wiring MovieSearchConfig movie_service.search

Participants (ssm-engine group aggregation)

Role Class Module
Strategy interface GroupAggregator (Protocol) strategies/group_aggregator.py
Concrete Strategy A LeastMiseryAggregator strategies/group_aggregator.py
Concrete Strategy B AverageAggregator strategies/group_aggregator.py
Concrete Strategy C MultiplicativeAggregator strategies/group_aggregator.py
Context combine_states endpoint app.py
Registry AGGREGATORS dict strategies/group_aggregator.py

UML Class Diagram

classDiagram
    class MovieSearchStrategy {
        <<interface>>
        +search(query, topK, minVoteCount) Optional~List~MovieDto~~
    }

    class SemanticSearchStrategy {
        -SsmEngineClient ssmEngineClient
        -MovieQueryService movieQueryService
        +search(query, topK, minVoteCount) Optional~List~MovieDto~~
    }

    class KeywordSearchStrategy {
        -MovieQueryService movieQueryService
        +search(query, topK, minVoteCount) Optional~List~MovieDto~~
    }

    class FallbackSearchStrategy {
        -MovieSearchStrategy primary
        -MovieSearchStrategy fallback
        +search(query, topK, minVoteCount) Optional~List~MovieDto~~
    }

    class MovieController {
        -MovieSearchStrategy movieSearchStrategy
        +semanticSearch(request) List~MovieDto~
    }

    class MovieSearchConfig {
        +movieSearchStrategy(semantic, keyword) MovieSearchStrategy
    }

    MovieSearchStrategy <|.. SemanticSearchStrategy : implements
    MovieSearchStrategy <|.. KeywordSearchStrategy : implements
    MovieSearchStrategy <|.. FallbackSearchStrategy : implements
    FallbackSearchStrategy o-- MovieSearchStrategy : primary
    FallbackSearchStrategy o-- MovieSearchStrategy : fallback
    MovieController --> MovieSearchStrategy : uses
    MovieSearchConfig ..> FallbackSearchStrategy : creates
    MovieSearchConfig ..> SemanticSearchStrategy : injects as primary
    MovieSearchConfig ..> KeywordSearchStrategy : injects as fallback
Loading

MovieSearchConfig is the only class that knows the concrete strategy types. MovieController is injected with only the MovieSearchStrategy interface and never references any concrete class.

Runtime Fallback Flow

flowchart TD
    A([POST /api/movies/semantic-search\nquery, topK, minVoteCount]) --> B[MovieController\n.semanticSearch]
    B --> C[FallbackSearchStrategy\n.search]
    C --> D[SemanticSearchStrategy\n.search — primary]
    D --> E{ssm-engine available\nand returned hits?}
    E -- Yes --> F([Return ranked\nvector results])
    E -- No / empty --> G[log: activating fallback]
    G --> H[KeywordSearchStrategy\n.search — fallback]
    H --> I{SQL LIKE matches?}
    I -- Yes --> J([Return keyword\nresults])
    I -- No --> K([Return empty list])
Loading

Each search() implementation returns Optional.empty() to signal "I have no answer — try the next strategy", rather than throwing an exception. FallbackSearchStrategy.search() inspects the Optional and delegates to its fallback strategy if empty. The controller receives a single Optional<List<MovieDto>> and never knows which strategy ran.

Group Aggregation Variant (ssm-engine)

The same pattern governs Watch Party scoring in Python. The GroupAggregator protocol defines a single aggregate(sim) method that receives a (num_candidates × num_participants) similarity matrix and returns a (num_candidates,) group-score vector.

classDiagram
    class GroupAggregator {
        <<Protocol>>
        +aggregate(sim ndarray) ndarray
    }

    class LeastMiseryAggregator {
        +aggregate(sim) ndarray
    }

    class AverageAggregator {
        +aggregate(sim) ndarray
    }

    class MultiplicativeAggregator {
        +aggregate(sim) ndarray
    }

    GroupAggregator <|.. LeastMiseryAggregator
    GroupAggregator <|.. AverageAggregator
    GroupAggregator <|.. MultiplicativeAggregator
Loading

The combine_states endpoint resolves the concrete aggregator from the AGGREGATORS registry using the string key supplied in the request body ("least_misery", "average", or "product"). A new scoring method requires only a new class and a single registry entry — zero changes to the endpoint logic.

Architectural Significance

The Strategy pattern enforces the proposal's core reliability guarantee: "Swap algorithms at runtime: Mamba → a simpler Blend Filter fallback if ML service is unavailable." Without it, the only option for handling ML unavailability would be a try/catch scattered through the controller. With the pattern, the controller is permanently closed to modification: a new strategy is a new class and a config change.


7.2 Pattern 2 — Observer (Asynchronous Event-Driven Decoupling)

Problem

Recording a watched movie — POST /api/user/{username}/watched-movies — is a latency-sensitive user action that must return immediately. However, it triggers a downstream side effect that is both slow and non-essential: the user's Mamba SSM state in recommendation-service must be updated. If this update were made synchronously, every "mark as watched" request would block on a 10-second HTTP round-trip to recommendation-service.

The Observer pattern solves this by separating what happened (the user watched a movie) from what should happen as a consequence (update the SSM state, audit-log the event, trigger notifications). The subject publishes an event and returns immediately; observers handle consequences independently, asynchronously, and without the subject knowing they exist.

Participants

GoF Role Class Location
Subject / Publisher UserProfileController user_service.controller
Event WatchedMovieAddedEvent user_service.event
Event Bus ApplicationEventPublisher Spring Framework
Concrete Observer RecommendationStateNotifier user_service.event
Target (notified system) UserSsmStateService recommendation_service

UML Class Diagram

classDiagram
    class ApplicationEventPublisher {
        <<Spring interface>>
        +publishEvent(event ApplicationEvent)
    }

    class UserProfileController {
        -ApplicationEventPublisher eventPublisher
        +addWatchedMovie(username, body, request) ResponseEntity
    }

    class WatchedMovieAddedEvent {
        -String username
        -String tmdbId
        +getUsername() String
        +getTmdbId() String
    }

    class RecommendationStateNotifier {
        -String recommendationServiceBaseUrl
        -HttpClient httpClient
        +onWatchedMovieAdded(event WatchedMovieAddedEvent)
    }

    class UserSsmStateService {
        +updateSsmState(username, tmdbId)
    }

    UserProfileController --> ApplicationEventPublisher : publishEvent()
    ApplicationEventPublisher ..> WatchedMovieAddedEvent : carries
    RecommendationStateNotifier ..> WatchedMovieAddedEvent : listens via @EventListener
    RecommendationStateNotifier --> UserSsmStateService : POST /internal/ssm-state/watched
Loading

UserProfileController depends only on ApplicationEventPublisher — it has no compile-time reference to RecommendationStateNotifier or any other observer. New observers (audit logging, analytics, push notifications) are independent @Component beans with @EventListener methods; adding one requires zero changes to the controller.

Runtime Sequence

sequenceDiagram
    actor User as Browser / User
    participant UC as UserProfileController
    participant DB as userdb (H2)
    participant EP as ApplicationEventPublisher
    participant RN as RecommendationStateNotifier
    participant RS as recommendation-service

    User->>UC: POST /api/user/{username}/watched-movies\n{ tmdbId, rating }
    UC->>UC: validate ownership (JWT check)
    UC->>DB: upsert WatchedMovie record
    UC->>EP: publishEvent(WatchedMovieAddedEvent)
    UC-->>User: 200 OK  ← immediate return

    Note over EP,RN: @Async — runs on background thread pool

    EP-->>RN: onWatchedMovieAdded(event)
    RN->>RS: POST /internal/ssm-state/watched\n{ username, tmdb_id }
    RS->>RS: call ssm-engine /update_state\npersist new SSM state to recommendation-db
    RS-->>RN: 200 OK
    Note over RN: Failure here is non-fatal — logged as warning
Loading

The 200 OK reaches the user before RecommendationStateNotifier begins its HTTP call. The @Async annotation on onWatchedMovieAdded ensures the listener runs on a separate thread from Spring's async executor. If recommendation-service is down, the warning is logged and swallowed — the user's watch record is already committed safely in userdb.

Extensibility: Adding a Second Observer

sequenceDiagram
    participant EP as ApplicationEventPublisher
    participant RN as RecommendationStateNotifier
    participant AN as BlendNotifier (future)
    participant AL as AuditLogger (future)

    EP-->>RN: onWatchedMovieAdded (async)
    EP-->>AN: onWatchedMovieAdded (async)
    EP-->>AL: onWatchedMovieAdded (async)
Loading

All three fire concurrently on separate async threads from the same event. The controller and WatchedMovieAddedEvent are unchanged.

Architectural Significance

Without the Observer, the controller would either block (adding 3–10 seconds to the most frequent user write action) or ignore the ML state update entirely (causing stale recommendations). The Observer gives both: the user gets an immediate acknowledgement, and the SSM state is updated in the background. It also enforces the open/closed principle for side effects: any future consequence of "a user watched a movie" is a new @EventListener bean — not a new line in the controller.


8. Architecture Analysis

8.1 The Two Architectures Compared

8.1.1 Implemented: Microservices + Async Event-Driven

StreamSync is deployed as 8 independently containerised units. Services communicate over HTTP REST; the user-service additionally publishes internal Spring events consumed asynchronously by RecommendationStateNotifier.

8.1.2 Alternative: Modular Monolith

A Modular Monolith is a single deployable unit structured into internal domain modules with enforced package boundaries but no network communication between them. For StreamSync this would mean: a single Spring Boot application with Java packages for auth, user, movie, recommendation, and social; the Python ML inference replaced by either a JVM-compatible ML library (e.g., Deep Java Library / ONNX Runtime) or an embedded Python subprocess.

C4Container
    title Alternative — Modular Monolith
    Person(u, "User")
    System_Boundary(ss, "StreamSync Monolith") {
        Container(spa,  "React SPA",  "Vite + React", "Browser UI")
        Container(mono, "StreamSync Server", "Spring Boot (single JAR)", "Auth · User · Movie · Recommendation · Social modules, co-hosted ML runtime (ONNX / DJL)")
        ContainerDb(db, "Shared Database", "PostgreSQL", "All domain tables in one schema")
    }
    Rel(u,    spa,  "HTTPS")
    Rel(spa,  mono, "HTTPS + Bearer JWT")
    Rel(mono, db,   "JDBC / JPA")
Loading
Property Microservices Modular Monolith
Deployable units 8 containers 1 JAR + 1 Postgres instance
Inter-service communication HTTP REST + Spring events (async) In-process method calls (0 ms overhead)
Technology boundary Java / Python split enforced at the wire Java only; ML constrained to JVM-compatible options
Datastore strategy Database-per-service (2 × Postgres, 2 × H2) Shared schema
Java source (non-test) ~3,300 lines across 46 files ~5,200 lines (same domain logic, no HTTP client boilerplate)
ML inference path hops 3 HTTP hops 0 network hops

8.2 Quantified Non-Functional Requirements

NFR-1 — ML Inference Latency (target: < 300 ms)

Offline pre-computation (the structural enabler)

The expensive work is done before any user request arrives:

  • build_index.py encodes all ~1M TMDB titles using sentence-transformers/all-MiniLM-L6-v2 (embedding dimension D = 384) into a FAISS IndexFlatIP. This rebuild takes ~30 minutes on an RTX 2060.
  • At query time, only the user's short natural-language string is encoded — a single forward pass through a 6-layer, 22M-parameter model.
  • Retrieval is an exact inner-product scan: O(N × D) = O(900,000 × 384) ≈ 345M multiply-accumulate operations, executed in <5 ms on CPU with AVX2 SIMD.

Per-request latency decomposition (microservices)

Step Operation Estimated Cost
1 Browser → Gateway (TLS + routing) ~2 ms
2 Gateway → movie-service (LAN HTTP) ~1 ms
3 movie-servicessm-engine (LAN HTTP) ~1 ms
4 ssm-engine: query embedding (all-MiniLM-L6-v2) ~15–40 ms
5 ssm-engine: FAISS IndexFlatIP.search(topK × 5) on 1M vectors ~3–8 ms
6 ssm-engine: quality re-ranking (5× topK candidates in memory) <1 ms
7 ssm-enginemovie-service (LAN HTTP, JSON payload) ~2 ms
8 movie-service: Postgres IN (tmdb_ids) lookup for enrichment ~5–15 ms
9 movie-service → Gateway → Browser ~2 ms
Total ~32–72 ms

Quantified comparison

Metric Microservices Modular Monolith Delta
Inter-service network overhead ~8 ms ~0 ms +8 ms (microservices)
Query embedding (CPU) ~15–40 ms ~15–40 ms 0 ms (identical model)
FAISS lookup (1M, D=384) ~3–8 ms ~3–8 ms 0 ms
Total typical latency (CPU) ~32–72 ms ~28–68 ms +4–8 ms (microservices)
Headroom vs 300 ms budget >228 ms >232 ms −4 ms (microservices)
Latency when ML path is down ~5–15 ms (SQL fallback) ~5–15 ms (SQL fallback) 0 ms

Conclusion for NFR-1: Both architectures meet the < 300 ms target comfortably. The microservices architecture pays an ~8 ms network overhead tax for separating the ML and Java runtimes. This is architecturally acceptable because the headroom is large (>220 ms) and the overhead is bounded and predictable on a LAN.

NFR-2 — Scalability and Independent Deployability

Each service has its own build context in docker-compose.yml. A code change to recommendation-service triggers a rebuild and restart of only that container:

docker compose up --build recommendation-service
Metric Microservices Modular Monolith
Independently deployable units 8 1
Average service size (Java) ~660 lines / service ~3,300 lines (whole app)
Rebuild scope after a logic change 1 service container Entire JAR
Independent GPU allocation Yes — ssm-engine only reserves GPU No — GPU must be provisioned for the single process
Startup health-gate dependency ssm-engine health check: up to 10 min (FAISS rebuild); other services unaffected Entire application unavailable until all modules including ML are ready
Scaling ML workloads independently Add ssm-engine replicas behind a load balancer; Java services untouched Must scale the entire monolith to add ML capacity
Technology diversity Java (Spring Boot) / Python (FastAPI + PyTorch + FAISS) Java only

GPU Isolation — A Concrete Scalability Advantage

ssm-engine in docker-compose.yml includes:

deploy:
  resources:
    reservations:
      devices:
        - driver: nvidia
          count: 1
          capabilities: [gpu]

This means GPU resources are allocated only to the ML container. The Python/CUDA ecosystem (torch, faiss-gpu, sentence-transformers) is the path of least resistance for production ML inference, and the microservices architecture is what makes this language boundary clean.


8.3 Trade-off Analysis

8.3.1 Operational Complexity

Microservices cost: A cold start of the full system can take up to 10 minutes while ssm-engine rebuilds the FAISS index over 1M titles. There are also 8 distinct process logs to monitor, 4 datastores to back up, and inter-service HTTP failures to diagnose — none of which exist in a monolith.

Modular monolith advantage: A single log stream, a single health endpoint, a single database backup, and in-process stack traces that cross module boundaries. Development velocity on a small team is meaningfully higher in the first weeks.

8.3.2 Fault Isolation

Microservices advantage: A crash in ssm-engine (Python OOM during FAISS rebuild, CUDA error) does not crash the Java services. movie-service falls back to KeywordSearchStrategy transparently. user-service continues recording watch history; the async RecommendationStateNotifier logs a warning and moves on.

Modular monolith cost: A Python OOM, a memory leak in the ML module, or a deadlock in the recommendation logic crashes or hangs the entire process. Auth, tracking, and social features go down alongside the ML failure.

In StreamSync's context this trade-off is especially acute: the FAISS build is a 30-minute, memory-intensive operation that runs on a scheduled 24h cycle. Running it in a shared process with the CRUD workload risks OOM-killing the entire application mid-day.

8.3.3 Development Velocity

Modular monolith advantage: In-process module calls mean no HTTP client code, no serialisation contracts, no depends_on health-check tuning, no container networking to debug. The Java services in StreamSync contain meaningful HTTP client boilerplate (SsmEngineClient, SsmEngineAdapter, NdjsonStreamProcessor, SsmBuildStatusPoller) — roughly ~300 lines of code that exists solely to manage inter-service communication.

Microservices cost: The Python/Java language boundary required explicit typed adapter classes and a streaming NDJSON protocol to move large datasets between services at startup.

8.3.4 Data Consistency

Microservices cost: When a user marks a movie as watched, three writes happen across service boundaries:

  1. user-serviceuserdb: persist WatchedMovie (synchronous, in the request).
  2. user-servicerecommendation-servicerecommendation-db: update SSM state (asynchronous, fire-and-forget).
  3. movie-servicemovie-db: no write, but catalog must reflect the title (populated at startup).

Steps 1 and 2 are not in a distributed transaction. This is an accepted eventual-consistency trade-off: the user's watch record is always durable; the ML recommendation quality lags by at most one watch event.

Modular monolith advantage: All writes occur in a single ACID transaction. The SSM state update and the watch record commit atomically; no eventual-consistency lag is possible.

8.3.5 Summary Scorecard

Quality Attribute Microservices Modular Monolith Winner
ML inference latency (NFR-1) ~32–72 ms (+8 ms network) ~28–68 ms Monolith (marginal)
Independent scalability (NFR-2) Strong — GPU isolated to ML containers Weak — must scale entire app Microservices
Fault isolation Strong — ML crash ≠ CRUD crash Weak — shared process Microservices
Development velocity (4 weeks) Moderate — HTTP boilerplate cost High — in-process calls Monolith
Operational simplicity Low — 8 containers, 4 datastores High — 1 process, 1 DB Monolith
Data consistency Eventual (async SSM update) Strong ACID Monolith
Technology flexibility High — Java + Python + FAISS Low — JVM only Microservices
Cold-start time Long (FAISS build gates services) Long (but single wait) Draw
ML model update with zero CRUD downtime Yes — restart ssm-engine only No — full restart Microservices

The microservices architecture wins on the attributes that matter most for StreamSync's long-term viability: independent GPU allocation, fault isolation between ML and CRUD, and the ability to iterate on the ML model without touching any Java service. The monolith wins on short-term development simplicity, operational overhead, and data consistency — all meaningful costs for a 4-person, 4-week project, but not architectural dealbreakers given the explicit mandate for independent deployability in the project proposal.


9. Individual Contributions

The following describes individual contributions towards the Prototype Development phase of the project.

Member Roll Number Contributions
Evan Bijoy 2023101080 Frontend, basic integration
Abhiram Tilak 2022113011 Foundational architecture, user DB, authentication
Adithya Kishor 2023111019 Python sidecar, ML, SSM, recommendation service
Anirudh Sankar 2023111024 User service, frontend integration, documentation

GitHub Repository: https://github.com/serc-project/stream-sync