SpotifUM
A JavaFX desktop music platform built on a rich object-oriented domain model, with listener, artist and admin experiences, playlist management, logical playback, analytics and backwards-compatible persistence.

Problem
How do you turn a console-driven, object-oriented Java application into a polished desktop product without destabilizing its existing domain model, persistence format and behavior? SpotifUM already had a real domain — users, artists, playlists, statistics — behind nothing but a text menu.
Solution
A layered JavaFX desktop app built on top of the untouched original domain, with three role-based experiences sharing one application core: Listeners get Home, Search, Library, playlist management, logical playback with a persistent player and a Statistics dashboard; Artists get a Studio for publishing and editing tracks and managing albums; Admins get a dashboard, user and artist management, catalogue administration, cross-account statistics and save/load. Views stay thin, controllers call application services with zero JavaFX imports, and every service calls the exact same domain managers the original console still uses today — so the CLI and the GUI never fork into two sources of truth. Ships with a bundled, entirely fictional demo library so every screen is populated on first launch.
Tech stack
- Java 23
- JavaFX
- Gradle
- JUnit 5
- Java Serialization
Screenshots






Architecture
JavaFX Views → Controllers → Application Services → the shared SpotifUM Facade / Managers → the original OOP Domain. Services (SessionService, PlaylistService, StatisticsService, AdminService, …) hold zero JavaFX imports and never re-implement a rule the domain already has; the console and the desktop app share one application core, so a change is visible to both interfaces and neither can drift from the other. Nothing about the original domain was rewritten — it was extended.
Technical highlights
- Three real polymorphic hierarchies driving behavior, not decoration: User (Free → Premium Base → Premium Top) decides playlist ownership and leaderboard rate by concrete class; Playlist splits into Premium/Favourite vs. Random; Music splits into ExplicitMusic and MultimediaMusic as real subtypes
- Logical playback — no bundled audio — with play/pause, next/previous, progress and playlist context, feeding the real Statistics subsystem on every play
- Statistics dashboard with real charts: top tracks/artists, genre distribution, a cross-account leaderboard and per-listener history
- Admin console mirroring the original CLI's admin capabilities exactly — user/artist/catalogue management, plan reassignment, persistence — nothing invented on top
- Backwards-compatible Java serialization: every persisted class keeps its original serialVersionUID, verified by a test that still loads a save file from the project's first commit
- 304 automated tests: domain, JavaFX-free service-layer, and dedicated identity/cascade/serialization regression tests
Technical decisions
- Kept the console interface running unmodified instead of retiring it, as living proof that the GUI evolution didn't fork the business logic into two versions.
- Resolved a stable-identity bug at the statistics layer with UUID-based lookups instead of changing User's global equals/hashCode, keeping the fix local to where the problem actually was.
- Resolved artist-rename attribution by the track's own stable id instead of the mutable display name stored on the track, so renaming an artist can't strand plays their older tracks already earned.
Challenges
- Backwards-compatible serialization: additive method changes on Serializable classes silently altered their auto-generated serialVersionUID. Recovered and pinned every affected class's original baseline UID, then added a regression test that loads a real save file captured from the project's first commit against every later build.
- Stable user identity: a subscription-plan change replaces a User's concrete class while its UUID stays the same, but the statistics maps were keyed on the object itself — splitting one logical account's history in two across an upgrade. Fixed by resolving by UUID at the statistics layer.
- Artist attribution after rename: Music stores its artist as a display-name string, so a rename could silently orphan a track's historical plays. Resolved by attributing plays through the track's own id instead.
- A latent bug in two Music subclasses where equals() called itself instead of delegating to the parent, so comparing two distinct instances recursed forever into a StackOverflowError. Fixed with a one-line delegation to Music.equals(), pinned by 4 regression tests.
- Ran a ~1,000-navigation stress pass after a scare during development to rule out a JavaFX memory leak: no accumulating View/Controller retention, only a small, bounded CSS-cache growth inherent to the JavaFX toolkit itself.
Results
- A complete JavaFX desktop experience across all three roles, with the original CLI still fully operational alongside it.
- 304 automated tests passing, including dedicated identity, cascade and serialization-compatibility regressions.
- A save file from the project's very first commit still loads correctly in the current build.
- A deterministic, populated demo dataset (15 artists, 88 tracks, 28 playlists, 357 recorded plays across 60 simulated days) so every screen shows real data immediately.
Future improvements
- A couple of narrower pre-existing domain quirks (e.g. exact-duplicate removal from a playlist) were deliberately left as documented rather than opportunistically fixed — the goal was safe evolution of an existing codebase, not a rewrite.
What I learned
- Most of the real work in evolving an existing codebase is archaeology: understanding exactly what a method already guarantees — equality, identity, serialization — before adding a single new screen on top of it.