Event-Driven Architecture
This diagram illustrates how BlueRobin uses NATS JetStream for asynchronous, event-driven document processing with guaranteed delivery.
Event Flow Sequence
sequenceDiagram
autonumber
participant API as Archives API
participant NATS as NATS JetStream
participant OCR as OCR Worker
participant Analysis as Analysis Worker
participant Chunking as Chunking Worker
participant Fanout as Embedding Fanout
participant Embed as Chunk Embedding
(per model)
participant Agg as Embedding Aggregator
participant NER as Entity Extraction
participant Graph as Graph Sync
participant Class as Classification
participant Blazor as Blazor Web
Note over NATS: Stream: ARCHIVES-DOCUMENTS
Retention: WorkQueue
MaxDeliver: 3
rect rgb(238, 233, 245)
Note right of API: Upload Phase
API->>NATS: Publish: documents.uploaded
{docId, bucket, fileName}
NATS-->>OCR: Deliver (ack required)
end
rect rgb(253, 248, 234)
Note right of OCR: OCR Phase
OCR->>OCR: Process with Docling
OCR->>NATS: Ack: documents.uploaded
OCR->>NATS: Publish: documents.ocr.completed
{docId, textLength, pageCount}
NATS-->>Analysis: Deliver
end
rect rgb(221, 212, 237)
Note right of Analysis: Analysis Phase
Analysis->>Analysis: Generate summary, keywords
via Ollama LLM
Analysis->>NATS: Ack: documents.ocr.completed
Analysis->>NATS: Publish: documents.analysis.completed
{docId, summary, keywords}
NATS-->>Chunking: Deliver
end
rect rgb(237, 245, 246)
Note right of Chunking: Chunking Phase
Chunking->>Chunking: Split into semantic chunks
Chunking->>NATS: Ack: documents.analysis.completed
loop For each chunk
Chunking->>NATS: Publish: documents.chunks.created
{docId, chunkIdx, text}
end
NATS-->>Fanout: Deliver chunk events
end
rect rgb(248, 237, 237)
Note right of Fanout: Embedding Fanout Phase
Fanout->>Fanout: Read chunk event
par Fan out to 8 models
Fanout->>NATS: Publish: embeddings.nomic
{chunkId, text}
Fanout->>NATS: Publish: embeddings.mxbai
{chunkId, text}
Fanout->>NATS: Publish: embeddings.snowflake
{chunkId, text}
Fanout->>NATS: Publish: embeddings.bge
{chunkId, text}
Note right of Fanout: ...and 4 more models
end
Fanout->>NATS: Ack: documents.chunks.created
NATS-->>Embed: Deliver to model workers
end
rect rgb(213, 238, 240)
Note right of Embed: Embedding Phase (parallel)
par 8 model workers processing
Embed->>Embed: Generate vector via Ollama
Embed->>Embed: Store in Qdrant collection
Embed->>NATS: Publish: embeddings.{model}.completed
{docId, chunkId, modelId}
Embed->>NATS: Ack: embeddings.{model}
end
NATS-->>Agg: Deliver completion events
end
rect rgb(213, 238, 240)
Note right of Agg: Aggregation Phase
loop Until all 8 models complete
Agg->>Agg: Track completion per doc
Agg->>NATS: Ack: embeddings.*.completed
end
Agg->>NATS: Publish: documents.embeddings.completed
{docId, modelCount, totalVectors}
NATS-->>NER: Deliver
end
rect rgb(253, 248, 234)
Note right of NER: Entity Extraction Phase
NER->>NER: Extract via Spacy NER
NER->>NATS: Ack: embeddings.completed
NER->>NATS: Publish: documents.entities.extracted
{docId, entityIds[]}
NATS-->>Graph: Deliver
end
rect rgb(237, 245, 246)
Note right of Graph: Graph Sync Phase
Graph->>Graph: Sync to FalkorDB
Graph->>NATS: Ack: entities.extracted
Graph->>NATS: Publish: documents.graph.synced
{docId, entityCount}
NATS-->>Class: Deliver
end
rect rgb(238, 233, 245)
Note right of Class: Classification Phase
Class->>Class: Classify via LLM
Class->>NATS: Ack: graph.synced
Class->>NATS: Publish: documents.classified
{docId, category}
end
rect rgb(213, 238, 240)
Note right of Blazor: Real-time Update
NATS-->>Blazor: Deliver: documents.classified
Blazor->>Blazor: Update UI component state
Blazor->>Blazor: Show "Document Ready" notification
endNATS JetStream Configuration
flowchart LR
subgraph Streams["JetStream Streams"]
S1["📬 ARCHIVES-DOCUMENTS
subjects: archives.documents.>"]
S2["📬 ARCHIVES-EMBEDDINGS
subjects: archives.embeddings.>"]
S3["📬 ARCHIVES-USERS
subjects: archives.users.>"]
end
subgraph Consumers["Consumer Groups"]
C1["OCR Consumer
filter: documents.uploaded"]
C2["Analysis Consumer
filter: documents.ocr.completed"]
C3["Chunking Consumer
filter: documents.analysis.completed"]
C4["Fanout Consumer
filter: documents.chunks.created"]
C5["Model Workers
filter: embeddings.{model}"]
C6["Aggregator
filter: embeddings.*.completed"]
end
S1 --> C1 & C2 & C3 & C4
S2 --> C5 & C6Delivery Guarantees
| Feature | Configuration | Purpose |
|---|---|---|
| AckPolicy | Explicit | Manual acknowledgment required |
| MaxDeliver | 3 | Retry up to 3 times on failure |
| AckWait | 30s | Time to process before redelivery |
| DeliverPolicy | All | Start from beginning on new consumer |
| ReplayPolicy | Instant | Process as fast as possible |
| MaxAckPending | 1000 | Backpressure control |
Error Recovery Pattern
flowchart TD
A[Message Received] --> B{Process OK?}
B -->|Yes| C[Ack Message]
B -->|No| D{Retries < 3?}
D -->|Yes| E[NAK with delay
exponential backoff]
D -->|No| F[Move to DLQ
dead-letter queue]
E --> G[Wait: 2^retry seconds]
G --> A
F --> H[Alert: Manual review
required]Benefits of Event-Driven Design
- Loose Coupling: Workers don't know about each other
- Scalability: Add worker replicas independently
- Resilience: Failed steps don't block pipeline
- Observability: Events provide audit trail
- Flexibility: Easy to add new processing steps