A four-step wizard for people considering a move to Berlin. Enter your salary, explore neighborhoods, plan your monthly budget with AI-powered analysis, and track the visa and admin process with an interactive checklist.
I built this as part of my 10-in-10 challenge (10 projects in 10 weeks). The motivation was twofold: I wanted real experience with Angular and Kotlin, and having gone through the Berlin relocation process myself, I wanted to build something genuinely useful for the people who keep asking me how it all works.
Salary Calculator. Enter your gross annual salary, tax class, age, church tax preference, and number of children. The backend runs the 2026 German Lohnsteuer algorithm and returns a complete breakdown with every deduction explained. For a EUR 60,000 gross salary, the calculator is accurate to within EUR 0.04/month compared to the official BMF calculator. The source was the BMF's own XML specification of the algorithm.
Neighborhood Explorer. All twelve Berlin Bezirke in a grid with culture descriptions, commute time ranges to Mitte (min/max based on actual BVG transit data), and key highlights. Select one to carry it into the budget step.
Cost Estimator & Budget Planner. Ten budget category sliders, an SVG donut chart, five sanity checks (rent ratio, savings rate, emergency fund, total allocation, grocery budget), and an AI-generated seven-section narrative about your financial life in that district. The AI analysis runs through OpenRouter. When the API key isn't configured or the call fails, a 555-line template engine generates all seven sections using real Bezirk-specific data. The app works fully without any AI key.
Visa & Admin Checklist. 19 items across four phases (pre-arrival through settling in), filtered by visa type (EU Blue Card, Freelance, Job Seeker). Each item links to the relevant government page and expands to show practical guidance. Progress persists to localStorage.
relocation-calculator/
├── frontend/ Angular 21 · TypeScript 5.9 · Tailwind 4
├── backend/ Kotlin 2.1 · Spring Boot 3.4 · JDK 21
└── shared/
└── api-contracts/
└── openapi.yaml ← single source of truth for all API types
The OpenAPI spec drives both sides. Gradle generates Kotlin data classes at compile time. openapi-typescript generates TypeScript types at build time. If the spec changes and either side doesn't account for it, compilation fails. Type mismatches are caught at build time, never at runtime.
State flows through a central WizardService backed by Angular Signals. A single effect() persists all wizard state to one localStorage key, so the entire wizard survives page reloads without a database.
All prefixed with /api/v1. Full schemas in shared/api-contracts/openapi.yaml.
| Method | Endpoint | What it does |
|---|---|---|
POST |
/salary/calculate |
Gross-to-net with full tax bracket breakdown |
GET |
/costs/estimate |
Rent, utilities, transport, groceries by Bezirk |
POST |
/costs/allocate |
Enrich budget percentages with EUR totals |
POST |
/costs/analyze |
7-section budget narrative (AI or template) |
GET |
/neighborhoods |
All 12 Bezirk profiles |
GET |
/neighborhoods/{bezirk} |
Single Bezirk profile |
docker-compose upFrontend at localhost:4200, backend at localhost:8080.
Backend:
cd backend
./gradlew bootRunStarts on port 8080. OpenAPI models generate automatically during compilation.
Frontend:
cd frontend
npm install
npm run generate:api
ng serveStarts on port 4200. API calls go to localhost:8080.
Set OPENROUTER_API_KEY as an environment variable to enable AI-powered budget analysis. Without it, the template engine handles everything. The model defaults to anthropic/claude-sonnet-4 and is configurable via OPENROUTER_MODEL.
| Layer | Platform | Trigger |
|---|---|---|
| Frontend | Vercel | Push to main |
| Backend | Railway | Push to main (Docker build, watches backend/** and shared/**) |
CORS on the backend allows *.vercel.app origins. The frontend reads the backend URL from frontend/src/environments/environment.prod.ts.
- Contract-first API development. Both sides generate types from one OpenAPI spec. No manual type mirroring.
- Signals over NgRx.
WizardServiceuses 10+ signals with a singleeffect()for persistence. RxJS is reserved for HTTP streams and formvalueChanges. - SVG donut chart with zero dependencies. Pure
<circle>elements withstroke-dasharraymath. OnPush-compatible, per-segment hover with CSS transitions. - Three-tier design token system. System primitives (
--reloc-sys-*) are raw values. Semantic references (--reloc-ref-*) remap between light/dark mode. Components only reference the semantic layer. - Template fallback for AI analysis. The app is fully functional without an API key. The template engine uses Bezirk-specific tips, rent quartile positioning, BVG pass cost comparisons, and emergency fund calculations.
- Commute time ranges, not single values. Berlin districts are large. A single number was misleading, so the API exposes min/max based on actual transit data.
| Technology | Version | |
|---|---|---|
| Frontend | Angular | 21.2 |
| TypeScript | 5.9 | |
| Tailwind CSS | 4.2 | |
| Backend | Kotlin | 2.1 |
| Spring Boot | 3.4 | |
| JDK | 21 | |
| Contracts | OpenAPI | 3.1 |
| AI | OpenRouter | Claude Haiku 3.5 (production) |
| Infra | Vercel + Railway | Frontend + Backend |