A full-stack train ticket booking platform with real-time seat availability, hold-then-pay booking flow, and Stripe payment integration. Built with Spring Boot, Next.js, and PostgreSQL.
- Trip Search — search trains between stations by date, with cached results via Valkey
- Live Seat Selection — real-time seat availability updates via Server-Sent Events (SSE)
- Hold-then-Pay Booking — reserve seats temporarily, then complete payment within a time window
- Stripe Payments — secure checkout with success/cancel handling and webhook confirmation
- User Accounts — JWT authentication with registration, login, payment history, and printable tickets
- Internationalization — Vietnamese (default) and English via next-intl
- OpenAPI Contract — auto-generated TypeScript client with TanStack Query hooks
| Layer | Technology |
|---|---|
| Backend | Java 25, Spring Boot 4.0, Spring Security, Hibernate ORM 7.2 |
| Frontend | Next.js 16, React 19, Bun, TypeScript, Tailwind CSS v4, shadcn/ui |
| Database | PostgreSQL 18, Flyway migrations |
| Cache | Valkey 8.0 (Redis-compatible) |
| Payments | Stripe |
| API Contract | SpringDoc OpenAPI + @hey-api/openapi-ts codegen |
| CI | GitHub Actions (lint, test, contract check, Docker validation) |
| Code Quality | Spotless (Java), Biome (TypeScript), ktlint (Gradle scripts), Lefthook pre-commit hooks |
cp .env.example .env
# Edit .env with your JWT_SECRET and Stripe keys
docker-compose up -dThe application will be available at:
- Customer App: http://localhost:41250
- Backend API: http://localhost:43565
- API Docs: http://localhost:43565/swagger-ui.html
Start the stack with the proxy profile to route frontend and API traffic
through a single Caddy origin:
docker-compose --profile proxy up -dWith the development Caddy configuration, the application is available at:
- Caddy entrypoint: http://localhost:2015
- Customer App through Caddy: http://localhost:2015
- Backend API through Caddy: http://localhost:2015/api
- Direct Customer App: http://localhost:41250
- Direct Backend API: http://localhost:43565
The proxy uses conf/Caddyfile.dev by default. It listens on
http://localhost:2015, forwards /api/* to the backend:8080 service, and
forwards all other paths to the customer:3000 service. The explicit http://
site address keeps local development on HTTP and disables automatic HTTPS.
For production, use conf/Caddyfile.prod as the mounted Caddyfile and set
CADDY_DOMAIN to the public domain. The production configuration enables
Caddy's automatic HTTPS, gzip compression, and forwarded client headers while
preserving the same /api/* backend routing and frontend fallback routing.
1. Start infrastructure services:
docker-compose up -d postgres valkey2. Start the backend:
cp .env.example .env
# Edit .env with your JWT_SECRET (minimum 32 characters)
cd backend
./gradlew bootRun3. Start the frontend:
cd frontend/customer
bun install
bun run devGenerate seed data for development:
cd scripts/generate_seed_data
# See README in this directory for usage.
├── backend/ # Spring Boot API (Java 25)
│ └── src/main/java/.../
│ ├── booking/ # Booking module (hold → pay flow)
│ ├── payment/ # Stripe payment integration
│ ├── station/ # Station management
│ ├── train/ # Trains, trips, seats, SSE
│ ├── user/ # Auth, registration, profiles
│ └── shared/ # AggregateRoot, Result monad, Money
├── frontend/
│ └── customer/ # Next.js 16 customer-facing app
│ └── src/
│ ├── app/[locale]/ # i18n routes (vi, en)
│ │ ├── (auth)/ # Login, register
│ │ ├── (main)/ # Search, trips, booking
│ │ └── (protected)/ # Account, payment, tickets
│ ├── components/ # UI components (shadcn/ui)
│ └── lib/api/ # Auto-generated API client
├── shared/
│ └── api-contracts/ # OpenAPI spec (source of truth)
├── build-logic/ # Gradle convention plugins
├── scripts/ # Dev utilities and seed data
└── docker-compose.yml
The backend follows Clean Architecture by Feature Module. Each module (booking, payment, station, train, user) has three layers:
- domain/ — pure Java entities, value objects, repository interfaces, domain events
- application/ — use cases, commands, queries, response DTOs
- infrastructure/ — JPA persistence, REST controllers, external service adapters
Architectural rules are enforced via ArchUnit tests.
cd backend
./gradlew bootRun # Start dev server (port 8080)
./gradlew test # Run all tests (requires Docker)
./gradlew test --tests '*.BookingTest' # Run a single test class
./gradlew spotlessApply # Format Java code
./gradlew spotlessCheck # Check formatting
./gradlew exportCustomerOpenApi # Export OpenAPI speccd frontend/customer
bun install # Install dependencies
bun run dev # Start dev server (port 3000)
bun run build # Production build
bun run test # Run Vitest tests
bun run lint # Biome check (lint + format)
bun run lint:fix # Biome auto-fix
bun run codegen # Regenerate API client from OpenAPI specWhen backend endpoints change:
pnpm run customer:sdk:generateThis exports the OpenAPI spec from the backend and regenerates the typed frontend client with TanStack Query hooks.
Formatting is enforced via Lefthook pre-commit hooks:
- Java: Spotless with Palantir Java Format (AOSP style)
- TypeScript/React: Biome (single quotes, 4-space indent, 80-char lines)
- Gradle scripts: ktlint
- SQL: Prettier
- Commits: Conventional Commits via commitlint
| Variable | Description |
|---|---|
JWT_SECRET |
JWT signing key (min 32 characters) |
JWT_ACCESS_TOKEN_EXPIRY |
Access token TTL in seconds (default: 900) |
JWT_REFRESH_TOKEN_EXPIRY |
Refresh token TTL in seconds (default: 604800) |
STRIPE_SECRET_KEY |
Stripe secret API key |
STRIPE_WEBHOOK_SECRET |
Stripe webhook signing secret |
STRIPE_SUCCESS_URL |
Redirect URL after successful payment |
STRIPE_CANCEL_URL |
Redirect URL after cancelled payment |
CORS_ALLOWED_ORIGINS |
Comma-separated allowed CORS origins for direct frontend access |
VALKEY_HOST |
Valkey/Redis host (default: localhost) |
VALKEY_PORT |
Valkey/Redis port (default: 6379) |
BOOKING_MAX_SEATS |
Maximum seats per booking (default: 5) |
- Backend: JUnit 5 + TestContainers (spins up PostgreSQL in Docker automatically)
- Frontend: Vitest + Testing Library + happy-dom
# Backend (requires Docker running)
cd backend && ./gradlew test
# Frontend
cd frontend/customer && bun run testGPL-3.0-only