This documents the complete request flow when an iOS user publishes a story (geo data) through the AdventureTube microservice architecture.
Design / Architecture
Fast Gateway Rejection — The 401 response takes only 4.275ms with a single span, meaning the Gateway’s JWT validation catches expired tokens before any downstream service is invoked.
Automatic Token Refresh — The iOS app handles 401 transparently — detects the error, refreshes the token, and retries the original request without user intervention.
Async Processing via Kafka — The geo/save endpoint returns 202 (Accepted) immediately after producing to Kafka. The actual MongoDB write happens asynchronously via the Kafka consumer. Both producer and consumer spans are visible in Zipkin.
SSE for Real-time Status — Instead of polling, the iOS app opens an SSE stream to receive job status updates in real-time (PENDING → PROCESSING → COMPLETED/DUPLICATE).
Sequence Diagram
Step 5 has no SSE and no StoryJobStatus. The screenshot Kafka message is fire-and-forget — iOS already received SSE COMPLETED in Step 4. Screenshot generation uses its own ScreenshotJobStatus that iOS will poll later. See Episode 2: Chapter Screenshots for the full screenshot flow.
iOS Call Stack: Publish Flow
The complete method call path from user action to UI update:
uploadStory() // AddStoryViewVM+Publishing.swift
│
├─ validaterAllContentsBeforeStoreToCoreData() // check all fields filled
├─ fetch StoryEntity from Core Data // find by youtubeId
├─ createJsonFromStory(storyEntity:) // serialize to JSON
│
├─ publishingStatus = .uploading
│
├─ publishStory(jsonData:) // AdventureTubeAPIService+Story.swift
│ │ // REST: POST /auth/geo/save
│ ├─ withTokenRefresh { accessToken in } // auto 401 retry
│ │ └─ session.dataTaskPublisher(for: request) // send HTTP request
│ │ └─ .tryMap → guard 200-299 → decode JSON
│ │ else → throw unauthorized/serverError
│ │
│ └─ .sink
│ ├─ receiveCompletion: { error → .failed }
│ │
│ └─ receiveValue: { response → got trackingId }
│ │
│ └─ startSSETracking(trackingId:, onCompleted:)
│ │
│ ├─ publishingStatus = .streaming
│ │
│ ├─ streamJobStatus(trackingId:)
│ │ ├─ SSEClient()
│ │ ├─ sseClient.connect(url:, headers:)
│ │ │ └─ URLSession(delegate: self)
│ │ │ └─ dataTask.resume()
│ │ └─ return sseClient.publisher
│ │ └─ .tryMap { data → JobStatusDTO }
│ │
│ └─ .sink
│ ├─ receiveCompletion: { error →
│ │ startPollingFallback()
│ │ └─ Timer.publish(every: 3.0)
│ │ └─ pollJobStatus() → handleJobStatus()
│ │ }
│ │
│ └─ receiveValue: { jobStatus →
│ handleJobStatus(jobStatus, onCompleted:)
│ │
│ ├─ .COMPLETED → cancelSSEStream()
│ │ → onCompleted(jobStatus)
│ │ ├─ isPublished = true
│ │ ├─ isStoryPublished = true
│ │ ├─ manager.save()
│ │ └─ publishingStatus = .completed → UI
│ │
│ ├─ .PENDING → keep waiting
│ ├─ .FAILED → .failed(message:)
│ └─ .DUPLICATED → .failed(message:)
│ }
Java Call Stack: Publish Flow
The complete method call path from controller to database save + async screenshot trigger:
AdventureTubeDataController.save() // POST /geo/save
│
├─ trackingId = UUID.randomUUID().toString()
├─ jobStatusService.createPendingJob(trackingId)
│ └─ save StoryJobStatus(PENDING) to MongoDB
│
├─ producer.sendAdventureTubeData(trackingId, data)
│ └─ KafkaMessage(trackingId, CREATE, data)
│ └─ kafkaTemplate.send("adventuretube-data", json) // async
│
└─ return 202 Accepted + ServiceResponse(pendingJob)
--- Kafka consumer thread (async) ---
StoryConsumer.consume(message)
│ └─ switch(CREATE) → handleSave(kafkaMessage, trackingId)
│
StoryConsumer.handleSave()
│
├─ adventureTubeDataService.save(data)
│ └─ adventureTubeDataRepository.save(data) // MongoDB insert
│
├─ jobStatusService.markCompleted(trackingId)
│ └─ SseEmitterManager sends COMPLETED event // → iOS SSE
│
└─ producer.sendScreenshotRequest(youtubeContentID, data)
└─ kafkaTemplate.send("adventuretube-screenshots") // async, no SSE
Complete Request Flow (Zipkin Traces)
Step 1: Initial POST — Token Expired (401)
POST /auth/geo/save — 401 CLIENT_ERROR (4.275ms, 1 span)
| Property | Value |
|---|---|
| Duration | 4.275ms |
| Services | gateway-service only |
| Spans | 1 |
| Status | 401 CLIENT_ERROR |
The iOS app sends the publish request with an expired JWT access token. The Gateway rejects it immediately — the request never reaches auth-service or any downstream service.
Step 2: Token Refresh (177ms)
POST /auth/token/refresh — 200 SUCCESS (177.184ms, 11 spans)
| Service | Span | Duration |
|---|---|---|
| gateway-service | http post /auth/token/refresh | 177.184ms |
| auth-service | http post /auth/token/refresh | 155.147ms |
| auth-service | http post (findtoken) | 27.791ms |
| member-service | http post /member/findtoken | 16.441ms |
| auth-service | http post (storetokens) | 121.238ms |
| member-service | http post /member/storetokens | 119.018ms |
Step 3: Retry POST — Publish Accepted (115ms)
POST /auth/geo/save — 202 ACCEPTED (115.044ms, 11 spans)
| Service | Span | Duration |
|---|---|---|
| gateway-service | http post /auth/geo/save | 85.134ms |
| auth-service | http post /auth/geo/save | 43.009ms |
| geospatial-service | http post /geo/save | 26.796ms |
| geospatial-service | adventuretube-data send | 38.509ms |
| geospatial-service | adventuretube-data receive | 20.265ms |
Step 4: SSE Status Stream (47ms)
GET /auth/geo/status/stream/{trackingId} — 200 SUCCESS (47.165ms, 9 spans)
| Service | Span | Duration |
|---|---|---|
| gateway-service | http get /auth/geo/status/stream/{trackingId} | 46.735ms |
| auth-service | http get /auth/geo/status/stream/{trackingId} | 32.788ms |
| geospatial-service | http get /geo/status/stream/{trackingId} | 19.976ms |
Performance Summary
| Step | Endpoint | Status | Duration | Spans | Services |
|---|---|---|---|---|---|
| 1. Initial Save | POST /auth/geo/save | 401 | 4.275ms | 1 | 1 (gateway) |
| 2. Token Refresh | POST /auth/token/refresh | 200 | 177.184ms | 11 | 3 (gw → auth → member) |
| 3. Retry Save | POST /auth/geo/save | 202 | 115.044ms | 11 | 4 (gw → auth → geo → kafka) |
| 4. SSE Status | GET /auth/geo/status/stream/{id} | 200 | 47.165ms | 9 | 3 (gw → auth → geo) |
| Total | 343.668ms | 32 |
Tech Stack
| Layer | Technology | Details |
|---|---|---|
| iOS Client | Swift | URLSession, SSE streaming, JWT token management |
| API Gateway | Spring Cloud Gateway | Routing, JWT validation, rate limiting |
| Auth Service | Spring WebFlux (Netty) | Reactive, non-blocking — proxies to member/geo services |
| Member Service | Spring MVC (Tomcat) | JPA/JDBC → PostgreSQL, Java 21 virtual threads |
| Geospatial Service | Spring MVC (Tomcat) | MongoDB, Kafka producer/consumer |
| Message Broker | Apache Kafka | Async event streaming between services |
| Tracing | Zipkin + Micrometer | Distributed tracing across all services |
