A high-throughput analytics backend processing 1,000,000+ events per day with end-to-end latency under 5 seconds. Built with Spring Boot, Apache Kafka, MySQL, and Redis. Features event ingestion pipeline, dimensional modeling, pre-aggregated metrics, and configurable report builder with CSV/PDF export.
Tech: Java · Spring Boot · Kafka · MySQL · Redis · ~5 weeks
System Architecture
Lambda-lite architecture: real-time stream processing (Kafka → Spring Boot consumers → Redis) + batch correction (nightly Spark jobs → MySQL). Dashboard queries hit pre-aggregated Redis for sub-100ms response; ad-hoc analysis queries MySQL.
Aggregation Workers → tumbling/hopping windows → Redis (pre-aggregated metrics) + MySQL (fact tables)
Dashboard API → Redis for real-time widgets, MySQL for historical/deep-dive
Batch Correction (nightly) → Spark → recompute from raw → fix late-arriving data in MySQL
Event Ingestion Pipeline
Kafka as the backbone: decouples producers from consumers, provides replayability, handles backpressure. Spring Boot consumers with exactly-once semantics via transactional producers.
@Configuration
public class KafkaConsumerConfig {
@Bean
public KafkaConsumer consumer() {
Map props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "analytics-ingestion");
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); // Exactly-once
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
return new KafkaConsumer<>(props);
}
}
Dimensional Modeling (Kimball)
Star schema in MySQL: fact tables for events, dimension tables for users, sessions, pages, products, geography. Surrogate keys, slowly changing dimensions (SCD Type 2 for user attributes), conformed dimensions across fact tables.
Schema overview
-- Fact table (partitioned by day)
CREATE TABLE fact_events (
event_id BIGINT UNSIGNED NOT NULL,
event_date DATE NOT NULL,
event_hour TINYINT UNSIGNED NOT NULL,
event_type_id SMALLINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED,
session_id BIGINT UNSIGNED NOT NULL,
page_id BIGINT UNSIGNED,
product_id BIGINT UNSIGNED,
geo_id INT UNSIGNED,
device_type_id TINYINT UNSIGNED,
revenue_usd DECIMAL(10,2) DEFAULT 0,
-- Measures
event_count BIGINT UNSIGNED DEFAULT 1,
PRIMARY KEY (event_date, event_id)
) PARTITION BY RANGE COLUMNS(event_date) (
PARTITION p2026_01 VALUES LESS THAN ('2026-02-01'),
PARTITION p2026_02 VALUES LESS THAN ('2026-03-01'),
-- ... monthly partitions
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- Dimension: User (SCD Type 2)
CREATE TABLE dim_user (
user_sk BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(64) NOT NULL, -- Natural key
email VARCHAR(255),
signup_date DATE,
plan_tier ENUM('free', 'pro', 'enterprise'),
-- SCD fields
valid_from DATETIME NOT NULL,
valid_to DATETIME,
is_current BOOLEAN NOT NULL DEFAULT TRUE
);
Conformed dimensions
dim_date, dim_geo, dim_device, dim_page, dim_product shared across all fact tables. Enables drill-across (e.g., revenue by geo + device).
Pre-aggregation in Redis
Real-time dashboards need sub-100ms queries. Pre-compute metrics in Redis with TTL-based expiry. Spring Boot aggregation workers consume from typed Kafka topics, compute windowed aggregates, write to Redis hashes/sorted sets.
@GetMapping("/api/v1/metrics/realtime")
public Map realtimeMetrics(@RequestParam String widget) {
return switch (widget) {
case "active_users" -> redisOps.opsForValue().get("gauge:active_users:5m");
case "top_pages" -> redisOps.opsForZSet().reverseRangeWithScores("top:pages:1h", 0, 9);
case "conversion_funnel" -> redisOps.opsForHash().entries("funnel:purchase:1h");
default -> Map.of("error", "Unknown widget");
};
}
Dashboard Query API
REST API serving two query paths: Redis (real-time, pre-aggregated) and MySQL (historical, ad-hoc). Unified response format; client unaware of source.
Endpoints
GET /api/v1/metrics/realtime?widget=active_users → Redis (sub-50ms)
GET /api/v1/metrics/historical?metric=pageviews&granularity=hour&from=2026-08-01&to=2026-08-25 → MySQL (partition pruning)
POST /api/v1/reports → Async report generation (CSV/PDF) via Celery-equivalent (Spring Batch)
GET /api/v1/dimensions/{dimension}/values → MySQL dimension tables (cached 1hr)
Report builder
Users configure reports via UI: select metrics, dimensions, filters, date range. API validates against metadata registry, generates SQL with parameter binding, executes via read replica.
Scaling Metrics
Production load: 1.2M events/day peak, 15K events/sec burst, <5s end-to-end (producer → dashboard). Cost: ~$350/month (3x Kafka brokers, 4x Spring Boot, MySQL db.r6g.xlarge, ElastiCache r6g.xlarge).
Metric
Value
Notes
Events/day (avg)
1.2M
Business hours 3x baseline
Peak throughput
15K events/sec
Kafka handles burst
End-to-end latency
<5s (p95)
Producer → Redis metric
Dashboard query (Redis)
<50ms p99
Pre-aggregated
Dashboard query (MySQL)
<500ms p95
Partitioned, indexed
Monthly cost
$350
AWS us-east-1
Key Architecture Decisions
ADR-001: Kafka over direct HTTP for ingestion
Decision: Kafka as ingestion buffer. Context: Producer variability (mobile batching, network issues), need replay for schema evolution. Trade-off: Added latency (~100ms) and operational complexity. Worth it for durability and replay.
ADR-002: Spring Boot over Node.js/Python for stream processing
Decision: JVM ecosystem for Kafka Streams / Spring Cloud Stream. Context: Exactly-once semantics, strong typing (Avro), team Java expertise. Trade-off: Higher memory baseline; longer startup. Mitigated by GraalVM native image for consumers.
ADR-003: Redis for real-time, MySQL for historical
Decision: Dual-query-path architecture. Context: Dashboard UX demands speed; analysts need flexibility. Trade-off: Data duplication, eventual consistency (Redis TTL + nightly reconciliation). Acceptable for analytics.
ADR-004: Dimensional modeling over wide tables
Decision: Kimball star schema. Context: BI tool compatibility, drill-across, conformed dimensions. Trade-off: More joins; MySQL handles well with proper indexes and partitioning.
Explore More
View other backend case studies or get in touch to discuss your project.