The projects I'm focused on right now.
Screenshot coming soon
In developmentArcClash
A turn-based, anime-inspired card-drafting duel game I'm building backend-first with Java 21 and Spring Boot 4. The rules engine and a first REST API with PostgreSQL persistence are implemented and tested; the playable game and its client are still to come.
JavaSpring BootREST APIsSpring Data JPAHibernatePostgreSQLFlywayMavenJUnit 5Mockito
Architecture, stack & roadmap
- 16
- JUnit tests
- 2
- REST endpoints
- 6
- Engine packages
Built so far
- ✓Rules engine in plain Java: character stats, a three-stance counter cycle, Duel Power calculation and duel resolution
- ✓Hidden five-character draft, magic cards with their effects, and battlefield events
- ✓Set and match flow for the Quick Duel and Championship modes
- ✓REST API to create a match and fetch it by id, persisted in PostgreSQL
- ✓Request validation (400) and not-found handling (404), covered by tests
Architecture
A modular monolith in one Spring Boot application. The match API follows Controller → Service → Repository with DTOs at the edges; the game rules live in a separate plain-Java engine with no Spring, HTTP or database code.
Controller · MatchController (@RestController) maps the routes and validates request bodies with @Valid
- ▼
DTOs · Java records: CreateMatchRequest (mode is @NotNull) and MatchResponse, so entities never leave the service layer
- ▼
Service · MatchService creates matches with UUID ids, maps entities to responses and raises 404 for unknown ids
- ▼
Repository · MatchRepository extends JpaRepository<MatchEntity, UUID>
- ▼
Database · MatchEntity on a PostgreSQL table created by a Flyway migration: UUID key, mode stored as text with a CHECK constraint, timestamp set by the database
- ▼
Game engine · Draft, duels, stances, magic, events and match coordination in plain Java. Not yet connected to the API
REST API
POST/api/matchesCreate a match. Body: { "mode": "QUICK_DUEL" | "CHAMPIONSHIP" }
201 Created with the new match id · 400 if mode is missing
GET/api/matches/{id}Fetch a match: mode, set wins for each player, finished flag, created time
200 OK · 404 if no match has that id
Technical decisions
- Rules engine without framework code
- The engine has no Spring or JPA imports, so the game rules are unit-tested quickly in isolation and can be reused by the API.
- Schema owned by Flyway
- Hibernate runs with ddl-auto=validate: it checks the entities against the migrated schema but never changes it.
- Open Session in View disabled
- spring.jpa.open-in-view=false keeps database access inside the service layer instead of during view rendering.
- Immutable DTOs and constructor injection
- Requests and responses are Java records validated with Bean Validation; dependencies are injected through constructors.
- Errors via ResponseStatusException
- A missing match maps to 404 with a clear reason. A global @ControllerAdvice for uniform error bodies isn't built yet.
Tech stack
- Backend
- Java 21Spring Boot 4Spring Web MVCBean Validation
- Data
- Spring Data JPAHibernatePostgreSQLFlyway
- Build & tests
- MavenJUnit 5MockitoMockMvc
Testing
16 JUnit 5 tests: a unit test for each engine component (stances, stats, Duel Power, duel resolution, draft, magic, events, sets and match flow), a context-load test, and four @WebMvcTest slice tests that use MockMvc with a Mockito-mocked service to check 201, 200, 400 and 404 responses.
Planned (not built yet)
- Connect the rules engine to the API, with match state persisted in PostgreSQL
- Clear, non-leaking error responses and duplicate-safe commands (from the design doc)
- Computer opponent for single-player matches
- React + TypeScript game client
- Spring Security once player accounts arrive
- Docker packaging and Testcontainers integration tests
- Real-time PvP over WebSockets
Screenshot coming soon
Not live yetAnimeGuess
An AI-powered anime character guessing game. Players ask yes/no questions about a hidden character and an AI answers from that character's stored profile, then they try to name them. It has several game modes, accounts, leaderboards and ranked real-time multiplayer: a Next.js frontend on a Spring Boot, PostgreSQL and Redis backend. It isn't live yet.
JavaSpring BootSpring SecuritySpring AIREST APIsWebSocketsSpring Data JPAHibernatePostgreSQLFlywayRedisNext.jsReactTypeScriptTailwind CSSDockerGitHub ActionsJUnit 5MockitoTestcontainersMavenVercelRender
Source code is private.
Architecture, stack & roadmap
- 21
- REST controllers
- 92
- Flyway migrations
- 28
- JPA entities
- 84
- Tests
What's built
- ✓Ask yes/no questions about a hidden character; an AI answers from that character's stored profile, then you guess who it is
- ✓Modes: Classic, Daily Challenge, Single Anime, Quick Game, Progressive Hints and Quiz (multiple choice with AI-written clues at three difficulty levels)
- ✓Typo-tolerant guess matching: "lufy" or just "luffy" both count for Monkey D. Luffy
- ✓Accounts with email verification, password reset, and Google or GitHub sign-in
- ✓Ranked real-time multiplayer with matchmaking and an Elo-style rating
- ✓Leaderboards, login streaks, achievements, friends and messages, plus a shop and avatar customisation
Architecture
A Next.js frontend talks to a layered Spring Boot API over REST and WebSockets. The backend is organised into controller, service, repository, entity, DTO, security, filter and exception packages.
Frontend · Next.js 14 and React 18 with TypeScript and Tailwind CSS; TanStack Query for API data, React Hook Form + Zod for forms, STOMP over SockJS for live games
- ▼
API · 21 @RestController classes over 37 services, documented with OpenAPI; a global exception handler returns one consistent error format
- ▼
Security · Stateless Spring Security: JWT authentication filter, BCrypt password hashing, OAuth2 login with Google and GitHub, reCAPTCHA on registration and guest games
- ▼
Real time · STOMP WebSocket endpoint with a SockJS fallback; the JWT is checked during the WebSocket handshake
- ▼
AI · Spring AI ChatModel calls OpenAI with a prompt template built from the character's profile
- ▼
Data · PostgreSQL through Spring Data JPA (28 entities, 92 Flyway migrations); Redis for rate limiting, live game state and leaderboard caching
Technical decisions
- Ground the AI in stored data
- Each answer is generated from the hidden character's own profile and restricted to "yes", "no" or "I'm not sure", so answers stay consistent and on-topic.
- Validate before calling the AI
- Questions are checked to be yes/no questions first, so open-ended questions never cost an AI call.
- Stateless auth everywhere
- The same JWT secures REST requests and the WebSocket handshake; there are no server sessions to share between instances.
- Schema in migrations
- All 92 schema changes are versioned Flyway migrations.
Challenges & solutions
- Keeping AI costs and abuse under control
- Bucket4j rate limits backed by Redis, so they hold across instances, with separate budgets for the AI question endpoint, sign-in, polling and other routes; reCAPTCHA v3 on registration and guest games; non-yes/no questions rejected before any AI call.
- Live multiplayer state
- Game conversations live in Redis with an expiry, and players join through JWT-authenticated STOMP connections.
- Fair ranked matches
- A matchmaking queue with a scheduler, and an Elo-style rating update (K-factor 32) after each game.
- Forgiving answers
- Fuzzy name matching allows one typo and partial names, with its own unit tests.
Tech stack
- Frontend
- Next.js 14React 18TypeScriptTailwind CSSTanStack QueryZod
- Backend
- Java 17Spring Boot 3Spring SecurityJWT & OAuth2WebSockets (STOMP)Spring AI
- Data
- PostgreSQLSpring Data JPAFlywayRedis
- Quality & delivery
- JUnit 5MockitoTestcontainersDockerGitHub ActionsSentry
Testing
84 tests: PostgreSQL integration tests on Testcontainers for authentication and the game service, @WebMvcTest controller tests with Mockito, and unit tests for question validation, guess matching, game rules and reCAPTCHA. GitHub Actions runs separate backend and frontend CI.