Portfolio Platform — Production-Grade Event-Driven Microservice Architecture

Year2026
TechnologyNext.js, TypeScript, Spring Boot, Kafka, PostgreSQL, pgvector, OpenSearch, Valkey, Gemini, OpenAI, MCP, Google Cloud Run, Vercel, Flyway
Project

Production Architecture Case Study · 2026

Portfolio Platform

Production-Grade Event-Driven Microservice Architecture

A full-stack portfolio platform designed as a real distributed system: event-driven content publishing, Kafka fan-out, OpenSearch indexing, RAG-powered AI assistant, privacy-aware analytics, and reliable notification delivery.

Core Patterns: Transactional Outbox · Kafka Fan-Out · Idempotent Consumers · Derived Projections · RAG · MCP Tool Governance · DLQ · Retry Backoff · Graceful Degradation


1. Executive Summary

This project is not a traditional static portfolio website. It is a production-grade distributed platform built around event-driven microservices, AI-assisted user interaction, real-time analytics, search indexing, notification delivery, and reliable asynchronous processing.

The portfolio website is only the user-facing surface. Behind it is a backend platform designed around the same engineering concerns found in real production systems:

  • Reliable content propagation through Kafka using the Transactional Outbox Pattern.
  • Search indexing through OpenSearch BM25, index-time query expansion, and query-time synonym expansion.
  • AI orchestration through an Agent Service, RAG, and MCP-based tool governance.
  • Analytics processing through an idempotent event pipeline with deduplication and privacy-aware aggregation.
  • Email notification delivery through a state-machine-based dispatcher with retry, backoff, and failure isolation.

Staff-Level Architecture Thesis

The platform intentionally separates the source of truth from derived projections. PostgreSQL owns canonical content. Kafka distributes change events. OpenSearch, pgvector, analytics rollups, and notification state are independently maintained projections that can be rebuilt, replayed, or repaired without corrupting the core content model.

The architecture prioritizes reliability, loose coupling, graceful degradation, observability, cost efficiency, and clear service ownership.

Core Principle: Keep the source of truth simple and reliable, then use events to build scalable, independent projections around it.


2. Product Context

The system supports five major user-facing capabilities:

2.1 Portfolio Browsing

Visitors can read projects, blogs, technical articles, life stories, and resume-related content.

2.2 Admin Content Management

The site owner can create, update, publish, and delete portfolio content through an admin dashboard.

2.3 Search

Users can search technical articles and projects using natural phrases, abbreviations, synonyms, and related technical concepts.

2.4 AI Chat Assistant

The chat assistant, “Mr. Pot,” answers user questions, retrieves relevant portfolio knowledge, classifies intent, and invokes backend tools only through a controlled MCP gateway.

2.5 Analytics and Notifications

The platform tracks page views, aggregates visitor activity, powers dashboard views, and sends email notifications when new content is published.


3. Architecture at a Glance

Area Design Choice Why It Matters
Write Path Admin Service + PostgreSQL + Outbox Protects the canonical content model from downstream failures.
Event Backbone Kafka Decouples content publishing from indexing, RAG, analytics, and notifications.
Search OpenSearch BM25 + generated search terms Improves recall without paying LLM cost at query time.
AI Agent Service + RAG + MCP Gateway Separates reasoning from execution and prevents unsafe direct backend mutation.
Analytics Kafka + Valkey SETNX + PostgreSQL rollups Provides at-least-once ingestion with idempotent aggregation.
Notifications Recipient state machine + scheduler Enables safe retry, per-recipient failure isolation, and provider throttling.

4. High-Level Architecture

flowchart TB Visitor["Visitor / Recruiter / Reader"] Frontend["Portfolio Frontend<br/>Next.js + Vercel"] AdminUI["Admin Dashboard"] ChatWidget["AI Chat Widget"] AdminService["Admin Service<br/>Content CRUD + Publishing"] AIService["AI Platform<br/>Agent + RAG"] MCP["MCP Gateway<br/>RBAC + Tool Governance"] Analytics["Analytics Platform<br/>Tracking + Rollups"] Notification["Notification Service<br/>Email Dispatch"] SearchIndexer["Search Indexer"] RAGIndexer["RAG Indexer"] Kafka["Kafka<br/>Event Streaming Backbone"] Postgres["PostgreSQL<br/>Content + Outbox + Metadata"] PgVector["pgvector<br/>RAG Embeddings"] OpenSearch["OpenSearch<br/>BM25 + Synonyms"] Valkey["Valkey<br/>Cache + Dedup + Rate Limit"] LLM["Gemini / OpenAI<br/>Intent + Chat + Enrichment"] Email["Email Provider"] Visitor --> Frontend Frontend --> ChatWidget ChatWidget --> AIService Frontend --> Analytics AdminUI --> AdminService AdminService --> Postgres AdminService --> Kafka Kafka --> SearchIndexer Kafka --> RAGIndexer Kafka --> Notification Kafka --> Analytics SearchIndexer --> OpenSearch SearchIndexer --> LLM RAGIndexer --> PgVector AIService --> PgVector AIService --> LLM AIService --> MCP MCP --> AdminService MCP --> Analytics Analytics --> Valkey Analytics --> Postgres Notification --> Postgres Notification --> Email Frontend --> OpenSearch

Architecture Principle

Each major capability is isolated behind an independently deployable service. Services communicate asynchronously through Kafka when eventual consistency is acceptable, and synchronously through APIs only when a user-facing request requires an immediate response.

This prevents one slow or failing downstream system from breaking the primary publishing workflow.


5. Service Ownership and Boundaries

Service Responsibility Owns Data? Communication Style
Frontend Portfolio UI, admin UI, chat widget, API proxy No HTTP
Admin Service Content CRUD, publishing workflow, content versioning, outbox events Yes HTTP + Kafka
Search Indexer Converts content events into OpenSearch documents No Kafka consumer
RAG Indexer Converts content into vector embeddings for AI retrieval No Kafka consumer
AI Platform Intent classification, chat orchestration, RAG, LLM calls Yes HTTP + MCP
MCP Gateway Tool validation, RBAC, idempotency, risk gating, audit logging Yes HTTP
Analytics Platform Event ingestion, deduplication, rollups, dashboard metrics Yes HTTP + Kafka
Notification Service Subscriber matching, email dispatch, retry lifecycle Yes Kafka consumer + scheduler

Design Insight: Avoid Shared Database Coupling

The most important boundary is that downstream services never directly mutate the Admin Service’s core content tables. The Admin Service owns canonical content. Search, RAG, notifications, and analytics consume events and build their own projections. This keeps the system evolvable and makes derived data rebuildable.


6. Core Design Goals

Functional Requirements

  • Publish projects, blogs, and portfolio content.
  • Index content for full-text and semantic-friendly search.
  • Generate RAG embeddings for AI chat.
  • Send content update notifications to subscribers.
  • Track page views and visitor activity.
  • Support AI assistant workflows through controlled backend tools.
  • Rebuild derived projections when search, RAG, analytics, or notification views become stale.

Non-Functional Requirements

Requirement Design Response
Reliability Transactional outbox, retries, DLQ, idempotent consumers
Scalability Kafka fan-out, independent consumers, stateless services
Low Latency Cached reads, async indexing, streaming AI responses
Cost Efficiency Index-time LLM enrichment, zero query-time LLM search
Consistency Strong local consistency for writes, eventual consistency for derived projections
Failure Isolation Search, RAG, notification, and analytics fail independently
Observability Structured logs, event IDs, audit trails, retry state
Privacy HMAC IP hashing, geo snapping, no raw IP persistence

Deep Dive 1 — Content Publishing Pipeline

7. Problem

When an admin publishes a project or blog, multiple downstream systems need to react:

  • Search must update the OpenSearch document.
  • RAG must regenerate embeddings.
  • Notification Service may email subscribers.
  • Analytics or audit systems may record the publishing activity.

A naive implementation would call these downstream systems synchronously inside the publish request:

Admin Service
  -> Save content
  -> Call Search Service
  -> Call RAG Service
  -> Call Notification Service

This design is fragile:

  • If Search is unavailable, content publishing may fail.
  • If Notification is slow, the admin request becomes slow.
  • If the database commit succeeds but Kafka publish fails, downstream systems miss the event.
  • If Kafka publishes but the database transaction rolls back, consumers may process a phantom event.

The system needs reliable event propagation without distributed transactions.

8. Solution — Transactional Outbox Pattern

The Admin Service writes the content row and the outbox event inside the same database transaction.

sequenceDiagram participant Admin as Admin Dashboard participant Service as Admin Service participant DB as PostgreSQL participant Publisher as Outbox Publisher participant Kafka as Kafka Admin->>Service: POST /api/admin/content Service->>DB: BEGIN TRANSACTION Service->>DB: INSERT / UPDATE content Service->>DB: INSERT content_version Service->>DB: INSERT outbox_event Service->>DB: COMMIT Service-->>Admin: 201 Created Publisher->>DB: Read unpublished outbox rows Publisher->>Kafka: Publish content event Kafka-->>Publisher: Ack Publisher->>DB: Mark event as published

The Admin Service does not need a distributed transaction across PostgreSQL and Kafka. PostgreSQL is the durable source of truth for the event until the publisher successfully sends it to Kafka.

9. Outbox Table Design

CREATE TABLE outbox_events (
    id UUID PRIMARY KEY,
    aggregate_type VARCHAR(64) NOT NULL,
    aggregate_id UUID NOT NULL,
    event_type VARCHAR(128) NOT NULL,
    event_version INT NOT NULL,
    payload JSONB NOT NULL,
    status VARCHAR(32) NOT NULL,
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_at TIMESTAMP,
    created_at TIMESTAMP NOT NULL,
    published_at TIMESTAMP
);

Example event:

{
  "eventId": "018f9f5a-7d52-7f43-9b7e-91b21a0cc921",
  "eventType": "CONTENT_PUBLISHED",
  "eventVersion": 1,
  "contentId": "project_123",
  "contentType": "PROJECT",
  "title": "Portfolio Platform",
  "slug": "portfolio-platform",
  "publishedAt": "2026-06-30T18:30:00Z"
}

10. Kafka Topic Design

content.search.index.v1
content.rag.index.v1
content.notification.feature_updates.v1
content.audit.activity.v1

The platform uses topic-level fan-out instead of one generic content topic because each downstream pipeline has different scaling, retention, replay, and failure-handling requirements.

Topic Consumer Purpose Operational Concern
content.search.index.v1 Search Indexer Update OpenSearch projection Replay for reindexing
content.rag.index.v1 RAG Indexer Generate embeddings Slower due to external AI calls
content.notification.feature_updates.v1 Notification Service Notify subscribers Strict deduplication required
content.audit.activity.v1 Audit pipeline Record publishing activity High retention and traceability

11. Why This Design Works

The Transactional Outbox pattern gives the system strong local consistency without distributed transactions.

  • If the database transaction rolls back, no outbox event exists.
  • If the database transaction commits, the event is durable.
  • If Kafka is temporarily unavailable, the outbox publisher retries.
  • If a consumer receives the same event more than once, the consumer deduplicates by event ID.

Architecture Decision

The platform chooses at-least-once delivery + idempotent consumers instead of trying to enforce global exactly-once behavior across PostgreSQL, Kafka, OpenSearch, external AI APIs, and email providers.

This is the practical production trade-off: reliable delivery with safe retries is more achievable and more debuggable than distributed exactly-once semantics.


Deep Dive 2 — Search Architecture

12. Problem

Portfolio search needs to handle more than exact keyword matching.

A user may search:

k8s
spring async event
vector search
slc kayak
system design outbox

The relevant documents may contain:

Kubernetes
event-driven architecture
pgvector
Salt Lake City
transactional outbox pattern

A basic SQL ILIKE search would miss many of these relationships. It also does not provide ranking, stemming, synonyms, or relevance tuning.

13. Solution — BM25 + Index-Time Expansion + Query-Time Synonyms

flowchart LR KafkaTopic["Kafka Topic<br/>content.search.index.v1"] Indexer["Search Indexer"] LLM["Gemini<br/>Doc2Query Expansion"] OpenSearch["OpenSearch Index"] UserQuery["User Query"] Analyzer["Query Analyzer<br/>Synonyms + Stemmer"] BM25["BM25 Multi-Match"] Results["Ranked Results"] KafkaTopic --> Indexer Indexer --> LLM LLM --> Indexer Indexer --> OpenSearch UserQuery --> Analyzer Analyzer --> BM25 OpenSearch --> BM25 BM25 --> Results

14. Three-Layer Recall Strategy

Layer 1 — BM25 Full-Text Ranking

OpenSearch provides standard full-text search and ranking through BM25.

The query uses weighted fields:

title^3
summary^2
search_terms^1.5
content^1

Title matches rank highest. Summary matches are also strong. Generated search terms improve recall. Body content provides broad coverage.

Layer 2 — Index-Time Semantic Expansion

When content is published, the Search Indexer consumes the Kafka event and builds an OpenSearch document.

{
  "id": "project_123",
  "type": "PROJECT",
  "title": "Portfolio Platform",
  "summary": "Production-grade event-driven microservice architecture",
  "content": "...",
  "search_terms": [
    "event driven architecture",
    "kafka outbox",
    "microservice portfolio",
    "spring boot kafka",
    "opensearch indexing",
    "rag search",
    "system design"
  ],
  "published_at": "2026-06-30T18:30:00Z"
}

The search_terms field is generated at index time using Gemini. This follows a Doc2Query-style approach: predict likely user search phrases when the document is indexed.

The benefit is cost control. The platform pays the LLM cost once during indexing instead of paying for every user query.

Layer 3 — Query-Time Synonym Expansion

OpenSearch applies a synonym graph filter at query time.

k8s => kubernetes
js => javascript
slc => salt lake city
rag => retrieval augmented generation

So a query like:

k8s deployment

can match content containing:

Kubernetes deployment

without calling an LLM during search.

15. Search Failure Handling

Search enrichment is designed to fail open.

  • If Gemini is unavailable, the indexer still writes the document to OpenSearch without generated search terms.
  • Publishing does not fail.
  • Basic search still works.
  • Search quality may temporarily degrade.
  • The event can be replayed later to regenerate enriched search terms.

Staff-Level Insight

Search enrichment is a quality improvement, not a correctness requirement. The source of truth remains PostgreSQL. OpenSearch is a projection. Therefore, the right behavior is not to fail publishing when enrichment fails, but to index the document minimally and repair enrichment asynchronously.


Deep Dive 3 — AI Agent and MCP Tool Governance

16. Problem

The chat assistant should answer user questions and interact with backend capabilities. However, allowing an LLM to call backend APIs directly is unsafe.

Potential risks include:

  • The model calls the wrong endpoint.
  • The model retries a mutation and creates duplicate side effects.
  • The model attempts a high-risk action without confirmation.
  • The model bypasses authorization.
  • Internal APIs become exposed through prompt injection or tool misuse.

The AI system needs to treat the LLM as a reasoning layer, not as a trusted execution layer.

17. Solution — Agent Service + MCP Gateway

sequenceDiagram participant User as User participant Widget as Chat Widget participant Agent as Agent Service participant LLM as Gemini / OpenAI participant MCP as MCP Gateway participant Tool as Backend Tool participant Audit as Audit Log User->>Widget: Ask question Widget->>Agent: SSE /api/agent/chat Agent->>LLM: Classify intent LLM-->>Agent: Intent + confidence alt Low confidence Agent->>LLM: Escalate to stronger model LLM-->>Agent: Refined intent end Agent->>Agent: Select allowed tool Agent->>MCP: Invoke tool with schema + idempotency key MCP->>MCP: Validate schema MCP->>MCP: Check RBAC MCP->>MCP: Check risk level MCP->>MCP: Deduplicate request MCP->>Tool: Execute backend action Tool-->>MCP: Result MCP->>Audit: Write structured audit event MCP-->>Agent: Tool result Agent-->>Widget: Stream answer chunks

The Agent Service handles reasoning and orchestration. The MCP Gateway handles execution governance.

The LLM never directly calls internal services.

18. Tool Governance Model

Every tool is registered with metadata:

{
  "toolName": "publishContent",
  "riskLevel": "HIGH",
  "requiredRole": "ADMIN",
  "idempotencyRequired": true,
  "schema": {
    "contentId": "string",
    "publish": "boolean"
  }
}
Risk Level Example Requirement
LOW Search content, read project info Allowed after authorization
MEDIUM Create draft, generate summary Logged and schema-validated
HIGH Delete content, bulk update, publish Explicit confirmation required

19. Why This Design Works

The MCP Gateway provides:

  • Schema validation.
  • Role-based access control.
  • Tool allowlisting.
  • Idempotency enforcement.
  • Risk-level gating.
  • Audit logging.
  • Duplicate call protection.

Architecture Decision

The LLM can reason about what should happen, but the gateway decides what is allowed to happen. This separates intelligence from authority, which is the key safety boundary for production AI systems.


Deep Dive 4 — Real-Time Analytics Pipeline

20. Problem

The platform tracks visitor activity to support:

  • Page view counts.
  • Content popularity.
  • Visitor location visualization.
  • 3D globe activity.
  • Time-based traffic trends.
  • Admin analytics dashboards.

However, analytics must be reliable and privacy-aware. The system should avoid storing raw IP addresses or overly precise user location data.

21. Analytics Data Flow

flowchart LR Browser["Browser"] TrackAPI["Tracking API<br/>/api/track"] Kafka["Kafka<br/>analytics.raw.events"] Aggregator["Analytics Aggregator"] Valkey["Valkey<br/>SETNX Dedup"] DB["PostgreSQL<br/>Rollup Tables"] Dashboard["Admin Analytics Dashboard"] Browser --> TrackAPI TrackAPI --> Kafka Kafka --> Aggregator Aggregator --> Valkey Valkey --> Aggregator Aggregator --> DB DB --> Dashboard

22. Event Schema

{
  "eventId": "018fa20b-2d61-7c84-9d30-144fd8f67a71",
  "eventType": "PAGE_VIEW",
  "path": "/projects/portfolio-platform",
  "referrer": "https://google.com",
  "userAgent": "...",
  "ipHash": "hmac_sha256(...)",
  "timestamp": "2026-06-30T18:45:00Z"
}

23. Deduplication Strategy

Kafka provides at-least-once delivery. That means the same event can be delivered more than once.

The aggregator uses Valkey SETNX to deduplicate:

SETNX analytics:event:{eventId} 1 EX 86400

If the key is created, the event is processed. If the key already exists, the event is skipped.

flowchart TD Event["Consume analytics event"] Dedup["SETNX eventId in Valkey"] IsNew{"Is new event?"} Skip["Skip duplicate"] Process["Parse user agent<br/>Hash IP<br/>Snap geo"] Upsert["UPSERT rollup row"] Commit["Commit Kafka offset"] Event --> Dedup Dedup --> IsNew IsNew -- No --> Skip --> Commit IsNew -- Yes --> Process --> Upsert --> Commit

24. Privacy Design

Data Strategy Reason
IP Address HMAC hash Enables approximate uniqueness without storing raw IPs
Location Snap to grid centroid Supports visualization while reducing fingerprinting risk
User Identity No persistent identity required for public visitors Avoids unnecessary tracking
Event ID UUIDv7 Time-sortable replay and deduplication
Aggregation Rollups over raw detail Optimizes dashboard queries and reduces sensitive data retention

25. Rollup Table Design

The system stores aggregated data at multiple granularities:

5-minute rollup
1-day rollup
content-level rollup
geo-level rollup

Example table:

CREATE TABLE analytics_geo_time_rollups (
    bucket_start TIMESTAMP NOT NULL,
    granularity VARCHAR(16) NOT NULL,
    geo_cell VARCHAR(64) NOT NULL,
    page_views BIGINT NOT NULL,
    unique_visitors BIGINT NOT NULL,
    PRIMARY KEY (bucket_start, granularity, geo_cell)
);

26. Graceful Degradation

If Kafka is unavailable, the tracking endpoint can fall back to a direct database insert.

  • Preferred path: Kafka ingestion.
  • Fallback path: Direct database insert.
  • Worst case: Analytics quality degrades, but portfolio browsing remains unaffected.

Staff-Level Insight

Analytics is important, but it is not on the critical user path. The platform should preserve the visitor experience even if analytics infrastructure is degraded. This is a deliberate prioritization of product availability over perfect telemetry completeness.


Deep Dive 5 — Notification Service

27. Problem

When a new project or blog is published, subscribers may need to receive an email notification.

The system must handle:

  • Many subscribers.
  • Provider failures.
  • Duplicate Kafka events.
  • Per-recipient retry.
  • Exponential backoff.
  • Open/read tracking.
  • Failure isolation.

A single Kafka event should not directly send all emails synchronously. That would make the publish flow slow and fragile.

28. Notification Fan-Out

flowchart TB Kafka["Kafka<br/>content.notification.feature_updates.v1"] Consumer["Notification Consumer"] DB["Notification DB"] Scheduler["Dispatch Scheduler"] Worker["Email Worker"] Provider["Email Provider"] Subscriber["Subscriber"] Kafka --> Consumer Consumer --> DB DB --> Scheduler Scheduler --> Worker Worker --> Provider Provider --> Subscriber

The Kafka consumer expands one content event into many recipient rows.

CONTENT_PUBLISHED event
        |
        v
Find matching subscribers
        |
        v
Insert notification_recipients rows
        |
        v
Scheduler claims due rows
        |
        v
Workers send emails

29. Email State Machine

stateDiagram-v2 [*] --> PENDING PENDING --> SENDING: Scheduler claims batch SENDING --> SENT: Provider success SENDING --> RETRY: Provider failure RETRY --> SENDING: next_retry_at reached RETRY --> FAILED: retry_count greater than max SENT --> READ: Tracking pixel opened FAILED --> [*] READ --> [*]

30. Dispatch Table

CREATE TABLE notification_recipients (
    id UUID PRIMARY KEY,
    notification_id UUID NOT NULL,
    subscriber_email VARCHAR(255) NOT NULL,
    status VARCHAR(32) NOT NULL,
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_at TIMESTAMP,
    provider_message_id VARCHAR(255),
    last_error TEXT,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL
);

31. Batch Claiming

The scheduler claims rows using a database-level lock:

SELECT *
FROM notification_recipients
WHERE status IN ('PENDING', 'RETRY')
  AND next_retry_at <= NOW()
ORDER BY created_at
LIMIT 100
FOR UPDATE SKIP LOCKED;

This allows multiple workers to run safely in parallel without sending the same email twice.

32. Backoff Policy

1st retry: 60 seconds
2nd retry: 120 seconds
3rd retry: 240 seconds
4th retry: 480 seconds
5th retry: 960 seconds
Then: FAILED

33. Why This Design Works

  • Subscriber fan-out is separated from provider delivery.
  • Each recipient has an independent state machine.
  • Provider failures do not block content publishing.
  • Retries are controlled by the database state, not in-memory worker state.
  • Multiple workers can safely claim work with FOR UPDATE SKIP LOCKED.
  • Failed rows remain inspectable and replayable.

Deep Dive 6 — Reliability and Failure Handling

34. End-to-End Delivery Semantics

The system intentionally uses at-least-once messaging with idempotent side effects.

This is more practical than trying to enforce global exactly-once behavior across databases, Kafka, APIs, and third-party providers.

flowchart LR Producer["Producer"] Outbox["Transactional Outbox"] Kafka["Kafka<br/>At-Least-Once"] Consumer["Consumer"] Dedup["Idempotency Check"] SideEffect["Side Effect<br/>Index / Email / Rollup"] Producer --> Outbox Outbox --> Kafka Kafka --> Consumer Consumer --> Dedup Dedup --> SideEffect

35. Failure Scenarios

Failure Handling System Behavior
DB transaction rolls back No outbox event is created No phantom downstream work
Kafka publish fails Outbox publisher retries Event remains durable in PostgreSQL
Consumer crashes before committing offset Kafka redelivers Consumer must be idempotent
Consumer receives duplicate event Idempotency key prevents duplicate side effects Safe replay
Search indexing fails Retry, then DLQ Search projection may lag
Gemini enrichment fails Index without enrichment Search remains functional
Email provider fails Retry with exponential backoff Per-recipient retry isolation
Poison event appears Route to DLQ Pipeline continues processing healthy events
Kafka unavailable for analytics Fall back to direct database insert Visitor experience preserved
Cache unavailable Process without cache when safe Reduced performance, not outage

36. Dead Letter Queue Strategy

Each major Kafka consumer has a DLQ topic:

content.search.index.dlq.v1
content.rag.index.dlq.v1
content.notification.dlq.v1
analytics.raw.events.dlq.v1

A DLQ message includes the original topic, event ID, consumer name, failure reason, retry count, and payload.

{
  "originalTopic": "content.search.index.v1",
  "eventId": "018fa20b...",
  "consumer": "search-indexer",
  "failureReason": "OpenSearch mapping conflict",
  "retryCount": 5,
  "payload": {}
}

The DLQ allows failed events to be inspected, fixed, and replayed without blocking the entire pipeline.


Deep Dive 7 — Data Consistency Model

37. Source of Truth vs Derived Projections

flowchart TB ContentDB["PostgreSQL<br/>Source of Truth"] Kafka["Kafka Events"] Search["OpenSearch<br/>Search Projection"] Vector["pgvector<br/>RAG Projection"] Notify["Notification DB<br/>Delivery Projection"] Analytics["Analytics Rollups<br/>Behavior Projection"] ContentDB --> Kafka Kafka --> Search Kafka --> Vector Kafka --> Notify Kafka --> Analytics

The Admin Service database is the source of truth for portfolio content.

OpenSearch, pgvector, notification rows, and analytics rollups are derived projections.

This means they can be rebuilt from source data and Kafka events if needed.

38. Consistency Trade-Off

The platform accepts eventual consistency for derived systems.

  • A blog may be published immediately.
  • Search may update seconds later.
  • RAG embeddings may update after indexing.
  • Notifications may send asynchronously.

The source of truth remains consistent immediately. Derived projections become consistent asynchronously.

Architecture Decision

The platform does not block the admin publish request on search indexing, embedding generation, analytics aggregation, or email delivery. Those are projections and side effects. Blocking on them would reduce reliability and increase user-facing latency.


Deep Dive 8 — Deployment Architecture

39. Runtime Deployment

flowchart TB subgraph Vercel["Vercel"] FE["Next.js Frontend"] end subgraph CloudRun["Google Cloud Run"] Admin["Admin Service"] AI["AI Platform"] SearchIdx["Search Indexer"] Analytics["Analytics Service"] Notify["Notification Service"] end subgraph ManagedData["Managed Data Infrastructure"] Kafka["Aiven Kafka"] OS["Aiven OpenSearch"] Redis["Aiven Valkey"] Supabase["Supabase PostgreSQL + pgvector"] end subgraph External["External APIs"] Gemini["Gemini API"] OpenAI["OpenAI API"] Email["Email Provider"] end FE --> Admin FE --> AI FE --> Analytics Admin --> Kafka Admin --> Supabase SearchIdx --> Kafka SearchIdx --> OS SearchIdx --> Gemini AI --> Supabase AI --> Gemini AI --> OpenAI Analytics --> Kafka Analytics --> Redis Analytics --> Supabase Notify --> Kafka Notify --> Supabase Notify --> Email

40. CI/CD and Database Migration

  • Docker containers for backend services.
  • GitHub Actions for CI/CD.
  • Google Cloud Run for service deployment.
  • Flyway for version-controlled database migrations.
  • Environment-specific configuration for development and production.

Database migrations are treated as part of the application release process rather than manual production changes.


41. Technology Stack

Layer Technology Purpose
Frontend Next.js, TypeScript, Vercel Portfolio UI, admin dashboard, chat widget
Backend Spring Boot 3, Java 21 Microservices
Messaging Kafka Event streaming, fan-out, async processing
Database PostgreSQL Content, outbox, notification state, analytics
Vector Search pgvector RAG embeddings
Search OpenSearch Full-text search, BM25, synonyms
Cache Valkey Deduplication, rate limiting, caching
AI Gemini, OpenAI Intent classification, chat, enrichment, embeddings
Tooling MCP Controlled AI tool execution
Infrastructure Cloud Run, Vercel, Flyway, GitHub Actions Deployment and migrations

42. Production Reliability Patterns

Pattern Used In Why It Matters
Transactional Outbox Admin Service Prevents DB/Kafka dual-write inconsistency
Idempotent Consumer Kafka consumers Makes redelivery safe
Dead Letter Queue Search, RAG, notification, analytics Isolates poison messages
Exponential Backoff Email dispatch Prevents provider overload
SETNX Dedup Gate Analytics Avoids duplicate rollup writes
Source-of-Truth + Projection Search and RAG Enables rebuildable derived data
Fail-Open Enrichment Search Indexer LLM failure does not block indexing
Graceful Degradation Analytics tracking Kafka failure does not lose all events
RBAC Tool Gateway AI Agent Prevents unsafe direct backend mutation
Audit Logging MCP Gateway Provides traceability for AI tool calls

43. Engineering Outcomes

This platform demonstrates several production-level engineering capabilities:

  • Designing event-driven systems with Kafka.
  • Applying the Transactional Outbox pattern correctly.
  • Building idempotent consumers and retryable pipelines.
  • Separating source-of-truth data from derived projections.
  • Implementing practical search using BM25, synonyms, and index-time expansion.
  • Designing safer AI agents with MCP governance.
  • Building real-time analytics with privacy-aware aggregation.
  • Designing reliable email dispatch with state machines and backoff.
  • Deploying independently scalable services using Cloud Run and managed infrastructure.

The result is a portfolio platform that functions as both a personal website and a living system design showcase.

It is intentionally built to reflect how production systems are designed: loosely coupled, observable, resilient to partial failure, and optimized around clear ownership boundaries.


44. Final Architecture Summary

flowchart LR Admin["Admin publishes content"] Outbox["Transactional Outbox"] Kafka["Kafka Fan-Out"] Search["Search Projection"] RAG["RAG Projection"] Notify["Notification Pipeline"] Analytics["Analytics Pipeline"] AI["AI Agent + MCP"] Admin --> Outbox Outbox --> Kafka Kafka --> Search Kafka --> RAG Kafka --> Notify Kafka --> Analytics AI --> RAG AI --> Notify AI --> Search

At a high level, the system follows one core principle:

Keep the source of truth simple and reliable, then use events to build scalable, independent projections around it.

That principle makes the architecture easier to scale, easier to debug, and safer to evolve over time.