PreScend is a free retirement savings calculator I built to cut through the noise of most financial planning tools — no account creation, no upsells, no email capture. Just open it and start planning. The tagline is "Cut the noise. Chart the climb."

This post covers the interesting technical decisions behind it: how the math works, how anonymous sessions are handled, and how it runs cheaply on AWS.


The Stack

Layer Technology
Backend Spring Boot 3.2, Java 17
Frontend Vanilla JavaScript (ES6+)
Charts Chart.js 4.4
Dev DB H2 (file-based)
Prod DB Amazon RDS PostgreSQL (db.t3.micro)
Deployment Docker → Amazon ECR → EC2 t3.micro
CI/CD GitHub Actions (self-hosted runner)

No framework on the frontend — no React, no Vue. Chart.js handles the visualizations and vanilla JS handles everything else. For a single-page calculator, the complexity of a frontend framework wasn't worth it.


What It Does

The app has five tabs:

  1. Retirement Tracker — the main dashboard. Add up to six account types (401k, Roth IRA, Traditional IRA, HSA, Brokerage, Savings), configure assumptions, and see your portfolio projected from today through end of life.
  2. Insights — personalized, auto-generated recommendations based on your accounts and configuration.
  3. Financial Blueprint — a step-by-step framework based on the r/personalfinance Prime Directive.
  4. Retirement Playbook — a readiness score (0–100) plus a detailed tax-optimized withdrawal strategy.
  5. Social Security Planner — compares claiming at 62, FRA, and 70 with break-even analysis.

The Projection Math

All calculations run client-side in JavaScript. The backend only stores and retrieves data — the server never touches the numbers.

Accumulation Phase

For each month from today until retirement:

// Apply monthly growth + contribution for each account
const monthlyRate = stockReturnRate / 100 / 12;
value = value * (1 + monthlyRate) + monthlyContribution;

Savings accounts use their own APY instead of the stock return rate. Everything else — 401k, IRAs, HSA, Brokerage — compounds at the configured expected return (default 7%, the S&P 500 historical average).

Retirement Phase

Once the accumulation phase ends, the withdrawal logic takes over. The base monthly withdrawal is derived from the user's estimated spending, inflated from today's dollars to the retirement date:

const inflFactor = Math.pow(1 + yearlyInflation / 100, yearsToRetirement);
const inflatedMonthlySpend = estimatedMonthlySpending * inflFactor;

Tax-deferred withdrawals (401k, Traditional IRA) need to be grossed up — if you want $4,500 after a 22% tax rate, you need to withdraw more:

const grossWithdrawal = netAmount / (1 - taxRate);

During retirement, spending continues to inflate each year:

const monthlyWithdraw = baseGrossTotal * Math.pow(1 + yearlyInflation / 100, Math.floor(month / 12));

Tax-Efficient Withdrawal Ordering

The "Per Account" chart mode uses a priority-ordered withdrawal strategy rather than drawing down all accounts proportionally. The order depends on retirement age:

Retire ≥ 59.5 (most common):

  1. Medical expenses → HSA first (triple tax advantage)
  2. General expenses → Savings → Brokerage → Traditional IRA → 401k → Roth IRA → HSA

The reasoning: Roth accounts have no Required Minimum Distributions and grow tax-free indefinitely, so they're the last resort. HSA funds used for medical expenses are completely tax-free — better than even Roth.

Retire before 59.5: the order shifts to avoid the 10% early withdrawal penalty. Rule of 55 applies to 401k if you leave your employer at 55+.


Retirement Readiness Score

The Playbook tab produces a score from 0–100 built from four weighted components:

Component Weight How It's Measured
Funding ratio 40 pts Projected balance at retirement vs. 4%-rule target
Savings rate 25 pts Current rate vs. 15% target
Tax diversification 20 pts Mix of Roth, Traditional, HSA, and Brokerage accounts
Longevity 15 pts Whether funds last through life expectancy

The 4%-rule target is the standard retirement planning heuristic: you need roughly 25× your annual expenses saved to safely withdraw 4% per year indefinitely.


Anonymous Sessions — No Login Required

This was the most deliberate design decision. Most financial tools require an account. I didn't want any friction, and I didn't want to store anything personally identifiable.

Sessions are identified by a one-way SHA-256 hash of the user's IP address combined with their User-Agent string:

String raw = ipAddress + ":" + userAgent;
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
// encode to hex string → sessionKey

The raw IP is never stored. The hash is non-reversible. A coarse geographic region (city + state + country) is derived from the first two IP octets via MaxMind GeoLite2 — enough for aggregate analytics, nothing more.

Every save creates an immutable snapshot — a new database row is appended, never updated. This gives a full change history for analytics and makes the data straightforward to reason about.

Data Retention

Storing every save indefinitely would grow unbounded. A nightly Spring scheduler trims old data:

@Scheduled(cron = "${retention.prune.cron}")
public void pruneOldSnapshots() {
    // Keep full history for snapshots <= 90 days old
    // For snapshots > 90 days old: keep only the most recent per session
}

This keeps storage flat regardless of usage volume, while preserving recent history for analytics.


Privacy & Data Export

The app collects anonymized aggregate data (intended for eventual sale on AWS Data Exchange). The export endpoint returns only GROUP BY aggregates — cohorts bucketed by age, savings level, location, etc. A few key rules:

  • k-anonymity ≥ 5 — any cohort with fewer than 5 members is suppressed entirely
  • No raw values ever exported — only averages and counts
  • Opted-out sessions excluded — the UserSession table has an optedOut boolean; those rows are skipped at query time

Users can opt out, export their own data as JSON, or request full deletion via dedicated endpoints — GDPR/CCPA compliance without needing a lawyer.


Deployment

The app runs on a single EC2 t3.micro ($8/month) backed by RDS PostgreSQL db.t3.micro ($14/month). Total: ~$22/month for a production-ready deployment.

The Dockerfile uses a multi-stage build — Maven compiles in one stage, the runtime image is Alpine-based and non-root:

FROM maven:3.9-eclipse-temurin-17-alpine AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
COPY --from=build /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

GitHub Actions builds the image on push to main, pushes to Amazon ECR, and deploys to the EC2 host via SSH. The self-hosted runner lives on the same EC2 instance, so there's no extra compute cost for CI.

Spring profiles handle the dev/prod split cleanly — application.properties points at H2 for local development, application-prod.properties reads database credentials from environment variables for RDS.

The t3.micro is a burstable instance. The two metrics to watch:

  • CPUCreditBalance — if it drains to zero, performance degrades
  • FreeableMemory — Spring Boot idles at ~350 MB; below 150 MB free is the signal to upgrade to t3.small

What I'd Do Differently

A few things I'd reconsider with hindsight:

localStorage + server-authoritative state works fine but creates edge cases when multiple tabs are open. A cleaner approach would be to treat the server as the single source of truth and drop the localStorage cache entirely — the 1.2s debounce already smooths out the UX.

Vanilla JS at 2,100+ lines in a single app.js starts to feel unwieldy. The calculation logic and the DOM manipulation are interleaved in ways that make testing hard. Separating them — even without a framework — would improve maintainability significantly.

No server-side rendering means the initial page load is a blank shell until JavaScript executes. For a calculator this is fine, but it rules out any SEO for the tool pages.