Zero Gravity Backend

Spring Boot REST API for emotion logging and AI analysis. Reduced Gemini payload by 97% through Time Bucket sampling, architected cloud infrastructure from scratch.

RoleBackend Developer
Timeline2025.10 - 2026.06Actively maintained
TeamSolo
SkillsSpring Boot, Java, MySQL, Docker, Terraform
Overview

In a Nutshell

A Spring Boot REST API that powers Zero Gravity's emotion logging and AI analysis. The infrastructure and deployment pipeline were built on a free-tier cloud, and the AI features were designed within budget constraints.

  • 5 domains, 14 endpoints: designed auth, emotion logging, statistics, and AI analysis into a single API.
  • Built infrastructure with OCI + Terraform 6 modules, and achieved Zero-Downtime deployment with a Build-first strategy.
  • Reduced Gemini API payload by 97% through Time Bucket sampling, keeping the AI feature within the free-tier budget.
Architecture

From team project to production.

A Spring Boot backend that started as a team project was taken to production. The project originally had only basic CRUD APIs. Authentication, infrastructure, a deployment pipeline, and AI analysis were all added on top.

  • Restructured the layered architecture into a domain-based structure.
  • Implemented NextAuth OAuth to JWT authentication.
  • Built infrastructure on OCI and implemented Zero-Downtime deployment.
  • Added Gemini API-based emotion analysis.

API Endpoints

DomainEndpointsDescription
AuthPOST /auth/verify, /auth/refreshOAuth token verification, JWT issuance and refresh
UserGET, DELETE /users/me, PUT /users/consent, POST /users/logoutProfile, account deletion, consent management, logout
EmotionPOST, GET, PUT /emotions/recordsCreate, read, and update emotion records
ChartGET /chart/level, count, reasonEmotion statistics by period (level, frequency, reason)
AIPOST /ai/emotion-predictions, GET /period-analysesEmotion prediction, period analysis

Infrastructure

OCI Cloud (Terraform 6 Modules)

Ampere A1 ARM64 (4 OCPU · 24GB)

Backend (docker-compose)

Frontend (docker-compose)

SSH + Docker

AWS Route53 zerogv.com · api.zerogv.com

GitHub Actions CI/CD · Zero-Downtime

Load Balancer HTTPS · TLS 1.3

Nginx Reverse Proxy · Rate Limit

Next.js 15 :3000

Spring Boot 3.2 :8080

MySQL 8.0 :3306

LayerTechnologyRole
IaCTerraform (6 modules)VCN · Compute · LB · Certificate · Storage · Monitoring
CloudOCI Ampere A1ARM64 instance
ProxyNginxReverse proxy · Rate Limit · Domain-based routing
ContainerDocker ComposeFrontend · Backend · MySQL isolation
SSLLet's EncryptACME DNS-01 via Route53 · Auto-renewal
CI/CDGitHub ActionsSSH + Docker Build → Health Check → Auto-Rollback
AI Token Optimization

Sending everything would blow the budget in a month.

The goal was to build a feature that analyzes emotion records via the Gemini API. Sending a full year of records meant ~55K input tokens per request. That was not sustainable on the project budget.

Reducing data would hurt analysis quality, but dropping the feature was not an option since it was core functionality.

One bucket, one representative record.

If sending everything was off the table, the solution was to pick the most representative record from each time period. The period was split into equal time units (buckets), and one representative record was taken per bucket. A Year analysis means monthly units, so twelve of them. Picking from the whole set at once would cluster them in the months with the most records, losing the shape of the year.

PeriodBucket UnitSamplesReduction
YearMonth12365 → 12 (97%)
MonthWeek4~30 → 4 (87%)

That left the question of how to define "representative." The criteria were already inside the service: the average emotion level and reason statistics the chart aggregated per period.

  • Closest to the average emotion level
  • Contains the most frequent reason

The reason statistics were re-aggregated per bucket, and the aggregation queries built for the emotion statistics were reused as they were.

Repeated per month · 12x

365 emotion records

Chart API aggregation monthly average level

Reason aggregation monthly top reason

Score that month's records level 60% + reason 40%

Highest score wins

12 representative records → sent to Gemini

Scored at 60% level, 40% reason.

Each bucket's representative was selected by a weighted score of emotion level 60% + reason match 40%.

private double calculateMatchScore(
    EmotionRecord record, Double targetLevel, String topReason
) {
    // Levels run 0-6, so the maximum distance is 6
    double levelScore = 1.0 - (Math.abs(record.getEmotionId() - targetLevel) / 6.0);
 
    double reasonScore = record.getEmotionReasons().contains(topReason) ? 1.0 : 0.0;
 
    return (levelScore * 0.6) + (reasonScore * 0.4);
}

Ties were broken by longest diary first, most reasons, then most recent.

Example: January bucket in a Year analysis (avg emotion level: 4.5, top reason: "Work")

RecordEmotionReasonsCalculationScore
ALevel 4Work, Family0.5 from the average + has Work0.95 ✅
BLevel 5Health0.5 from the average, no Work0.55
CLevel 2Work2.5 from the average + has Work0.75

97% payload reduction, $0.002 per request

MetricBefore (Full Payload)After (Sampled)
Annual analysis payload365 records12 records (97% reduction)
Input tokens per request~55K tokens~2.4K tokens
Cost per request$0.017$0.002

Note: AI analysis results were cached for 24 hours, and the cache was invalidated for the relevant period whenever emotion records changed.

arrow_downwardExplore Furtherarrow_downward