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:
- 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.
- Insights — personalized, auto-generated recommendations based on your accounts and configuration.
- Financial Blueprint — a step-by-step framework based on the r/personalfinance Prime Directive.
- Retirement Playbook — a readiness score (0–100) plus a detailed tax-optimized withdrawal strategy.
- 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):
- Medical expenses → HSA first (triple tax advantage)
- 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
UserSessiontable has anoptedOutboolean; 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.