Backend

Music Catalog Service

A Spring Boot REST service that demonstrates how to build a maintainable, secure, and well-tested backend system using modern Java engineering practices.

Published: August 4, 2026Updated: August 4, 2026Difficulty: Intermediate
Music Catalog Service cover

Project Snapshot

Category
Backend
Language
Java 25
Framework
Spring Boot
Database
PostgreSQL
Build Tool
Maven
Repository
Public

Additional Technologies

Spring SecuritySpring Data JPAFlywayJWTTestcontainersDocker ComposeOpenAPI / SwaggerSpring Boot Actuator

What This Project Covers

Music Catalog Service is a REST API for managing artists and their track catalogues. The project is designed as a learning case study, not a toy application. Every implementation decision reflects a real production concern — from how the database schema evolves over time, to how authentication is enforced, to how tests gain confidence without relying on in-memory databases.

The service supports the following operations:

  • Add a new track to an artist's catalogue
  • Update an artist's name
  • Retrieve all tracks for a specific artist
  • Display a rotating Artist of the Day

By studying this project you will see how these individually simple requirements translate into a codebase that is secure, observable, and safe to change.


Project Structure

The codebase is organised by feature rather than by technical layer. This keeps business capabilities cohesive and avoids the common problem of having dozens of unrelated classes bundled together in a service/ or repository/ directory.

com.deeptechhub.musiccatalog
├── artist/
│   ├── ArtistController.java
│   ├── ArtistService.java
│   ├── ArtistRepository.java
│   ├── Artist.java
│   ├── ArtistDTO.java
│   └── ArtistMapper.java
├── track/
│   ├── TrackController.java
│   ├── TrackService.java
│   ├── TrackRepository.java
│   ├── Track.java
│   ├── TrackDTO.java
│   └── TrackMapper.java
├── artistoftheday/
│   ├── ArtistOfTheDayController.java
│   └── ArtistOfTheDayService.java
├── common/
│   ├── config/
│   ├── exception/
│   ├── security/
│   └── web/
└── MusicCatalogServiceApplication.java

Each feature folder owns everything that feature needs: its controller, service, repository, entity, DTOs, and mapper. Adding a new feature means adding a new folder — not scattering changes across multiple technical layers.


API Design

The API follows REST conventions with a versioned base path /api/v1. All endpoints return JSON. Error responses use Spring's ProblemDetail format for a consistent error contract across all endpoints.

MethodEndpointDescriptionAccess
POST/auth/loginAuthenticate user, returns JWTPublic
POST/artistsCreate artistAdmin
GET/artistsList all artistsPublic
GET/artists/{artistId}Get artist detailsPublic
PATCH/artists/{artistId}/nameUpdate artist nameAdmin
POST/artists/{artistId}/tracksAdd track to artistAdmin
GET/artists/{artistId}/tracksList tracks for artistPublic
GET/artist-of-the-dayGet today's featured artistPublic

Read operations are publicly accessible. Write operations require an authenticated ADMIN user. This separation is enforced at the Spring Security configuration level, not scattered across individual service methods.

Swagger UI is available locally after startup:

http://localhost:8080/swagger-ui.html

Security Design

The service uses stateless JWT authentication with Role-Based Access Control.

Authentication flow

  1. The client sends credentials to POST /api/v1/auth/login.
  2. The service validates the credentials against the database and returns a signed JWT.
  3. The client includes the token in subsequent requests:
http
Authorization: Bearer <jwt-token>
  1. Spring Security validates the token on every request — no session state is stored server-side.

Authorisation model

Security rules are defined in a central Spring Security configuration class. Public endpoints are explicitly whitelisted; everything else requires authentication. Admin-only write operations use method-level security annotations.

This makes the security posture easy to audit: there is one place to look at what is public, and one place to look at what requires privilege.

Authentication is implemented within the application rather than as a dedicated service. A dedicated auth service would add operational complexity that is out of scope for a single-service case study.


Database Design

UUID primary keys

All entities use UUID primary keys. This avoids exposing sequential integer identifiers in API responses and allows identifier generation to happen independently of the database — useful for testing and for distributed architectures.

Flyway migrations

Database schema changes are managed through versioned Flyway migration scripts. Hibernate's schema generation is disabled and set to validation only — so if the application starts and the schema does not match the entity definitions, the startup fails rather than silently altering the database.

This makes database changes explicit and auditable:

db/migration/
  V1__create_artists_table.sql
  V2__create_tracks_table.sql
  V3__add_artist_of_day_index.sql

Optimistic locking

Mutable entities carry a @Version column. When two concurrent requests attempt to update the same entity, one will succeed and the other will receive a conflict error rather than silently overwriting a change.

Genre as an enum

Genres are modelled as a Java enum and stored as strings. A database constraint ensures only valid genre values can be persisted. This is a practical example of using the type system and the database together to enforce business rules. The enum-with-lambda-strategy-pattern guide explores this Java pattern in depth.


Artist of the Day Algorithm

The featured artist rotates through the catalogue in a fair, deterministic cycle — with no scheduled jobs, no random state, and no dependency on caches.

java
long epochDayUtc = LocalDate.now(ZoneOffset.UTC).toEpochDay();
int offset = (int) Math.floorMod(epochDayUtc, totalArtists);
// Artists ordered by created_at, id — same order every call

Because the offset is computed from the calendar day (not wall clock time), the result is:

  • Consistent across all application instances
  • Unchanged across application restarts
  • Guaranteed to cycle through every artist before repeating

This is a good example of solving an operational requirement through pure computation rather than infrastructure.


Testing Strategy

The project uses a layered testing approach that gives high confidence without sacrificing speed.

Unit tests

Business logic is tested in isolation with JUnit 5 and Mockito. These tests run fast and validate the core domain rules independently of the database or HTTP layer.

Integration tests with Testcontainers

Integration tests use Testcontainers to spin up a real PostgreSQL container:

java
@SpringBootTest
@Testcontainers
class ArtistControllerIT {
 
    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");
 
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
 
    // ...
}

This validates end-to-end behaviour — API contract, persistence, Flyway migration execution, and database constraints — against a production-equivalent database engine.

The integration tests are not substitutes for unit tests. Each layer tests what it is best at: units test logic; integration tests test boundaries.


Observability and Operations

Health and metrics

Spring Boot Actuator exposes operational endpoints used for monitoring:

EndpointPurpose
/actuator/healthApplication health status
/actuator/infoApplication metadata
/actuator/metricsJVM and application metrics
/actuator/prometheusPrometheus-compatible scrape endpoint

The Prometheus endpoint makes it straightforward to connect the service to a Grafana dashboard for visualising request rates, error rates, and JVM health.

Correlation ID tracing

Every request supports an X-Correlation-Id header. If the client provides one, it is propagated through the application and included in all log output for that request. If no correlation ID is provided, the service generates one.

This is a lightweight but practical step towards distributed tracing — it makes it possible to isolate all log entries belonging to a single request even when logs from multiple requests are interleaved.

The Spring Boot Correlation ID article on DeepTechHub covers this pattern in detail.


Running Locally

The only prerequisite is Docker. The application and its PostgreSQL database run inside Docker Compose.

bash
docker compose up --build

The API is then available at http://localhost:8080.

A default administrator account is created during database initialisation for local development:

UsernamePassword
adminadmin123

Authenticate with:

bash
curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "admin123"}'

Use the returned JWT token in the Authorization: Bearer <token> header for admin endpoints.

Running tests

bash
# Unit tests only
./mvnw test
 
# Full suite including Testcontainers integration tests (requires Docker)
./mvnw verify

Key Learning Areas

REST API Design
Package-by-Feature Architecture
JWT Authentication & RBAC
Flyway Database Migrations
Testcontainers Integration Testing
Spring Boot Actuator & Prometheus
UUID Primary Keys & Optimistic Locking
ProblemDetail Error Handling
Correlation ID Request Tracing
OpenAPI Documentation

Related Content

Continue learning with supporting DeepTechHub articles and learning paths.