A Spring Boot microservice for managing crm master setup data.
- Java 21
- Spring Boot 3.5.15 (Web, Data JPA, Validation, Security, OAuth2 Resource Server)
- PostgreSQL (
postgresqlJDBC driver) - Redis
- Liquibase (schema migrations)
- MapStruct 1.5.5 (entity/DTO mapping)
- Lombok
- Maven
- Java 21+
- Maven 3.9+ (or use the included Maven Wrapper)
- Docker & Docker Compose (for the PostgreSQL database and the Redis cache)
Connection and server settings live in src/main/resources/application.yaml.
Defaults:
| Setting | Default |
|---|---|
| Server port | 8202 |
| Datasource URL | jdbc:postgresql://localhost:5432/crm |
| Username | crm_master_setup |
| Schema | crm_master_setup |
Liquibase runs on startup using classpath:db/changelog/master.xml to create the
MASTER SETUP tables, sequence, and indexes. JPA ddl-auto is none — the schema is
owned entirely by Liquibase.
A docker-compose/postgress/docker-compose.yml at the project root starts a PostgreSQL 16 instance
(container postgres_db) with a superuser admin / admin and a crm
database, exposed on port 5432:
docker compose up -dThe compose file only creates the admin superuser and crm database. The
application connects as a dedicated crm_master_setup role that owns its own schema,
so after the container is up, create the role and schema once:
CREATE USER crm_master_setup
WITH PASSWORD 'admin';
CREATE SCHEMA crm_master_setup
AUTHORIZATION crm_master_setup;
Run them against the crm database.
With the role and schema in place, Liquibase can create its tracking tables and apply the changelog on the next application start.
docker-compose/redis/docker-compose.yml starts Redis 7 (container redis,
password admin, AOF persistence on a named volume) on 127.0.0.1:6379, plus
RedisInsight on http://localhost:5540 for browsing keys:
docker compose -f docker-compose/redis/docker-compose.yml up -dspring:
data:
redis:
host: localhost
port: 6379
password: admin
timeout: 2s
connect-timeout: 2sspring:
cache:
type: redis
redis:
time-to-live: 24h
key-prefix: "crm-master-setup::"
use-key-prefix: true
cache-null-values: false| Class | Job |
|---|---|
CacheConfig |
@EnableCaching entry point. Builds a RedisCacheConfiguration from the spring.cache.redis.* properties above, then registers per-cache overrides with a RedisCacheManagerBuilderCustomizer |
LoggingCacheErrorHandler |
CacheErrorHandler that logs get/put/evict/clear failures at warn instead of rethrowing — a Redis outage degrades to a database read rather than a 500 |
# Run the app
./mvnw spring-boot:run
# Build a jar
./mvnw clean package
# Run tests
./mvnw testThe service starts on http://localhost:8501.
A Dockerfile is provided that packages the built JAR on top of an
eclipse-temurin:21-jre-alpine base image. Build the JAR first, then the image:
./mvnw clean package
docker build -t crm-master-setup .Run the container, mapping the service's port (8202):
docker run --rm -p 8501:8501 crm-master-setupThis service is a pure OAuth2 resource server — it validates bearer JWTs issued by a Keycloak realm on every request. There's no login flow or session state here; Keycloak owns authentication, this service only enforces it.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8180/realms/crm-realmSpring uses that issuer to auto-discover Keycloak's JWKS and verify each
token's signature, expiry, and issuer. Only /api/** is locked down
(SecurityConfig); everything else is permitAll(). Sessions are stateless
and CSRF is disabled — bearer tokens don't need either.
Keycloak ships roles under realm_access.roles, not the scope claim Spring
expects by default, and hands you a raw Jwt rather than a domain object. Two
small converters close that gap:
| Class | Job |
|---|---|
KeycloakRealmRoleConverter |
maps realm_access.roles → ROLE_* authorities |
UserPrincipalJwtConverter |
builds a typed UserPrincipal (userId, username, email, name, department, phone, mobile) as the authentication's principal |
Anywhere in the codebase, SecurityUtil.getUserPrincipal().
Role names live as constants in SecurityRoles (ORG_ADMIN, SALES_REP,
ACCOUNT_MANAGER, MARKETING_MANAGER, SALES_MANAGER), ready for per-endpoint
@PreAuthorize checks as they're added. Today the security filter chain only
requires an authenticated principal on /api/** — it does not yet
differentiate access by role.
A Jenkinsfile defines a declarative pipeline that:
- Build — runs
mvn clean package -DskipTestsand archives the resulting JAR. - Test — runs
mvn testand publishes the JUnit surefire reports. - Docker Build & Push — logs in to Docker Hub, builds the image tagged with
the Jenkins
BUILD_NUMBER, and pushes it, then removes the local image afterward.
The pipeline requires JDK 21 (jdk-21) and Maven (maven-3.9) tool
installations configured in Jenkins, plus a docker-hub-credentials
username/password credential. Update the DOCKER_HUB_USER build parameter (or
its default) in the Jenkinsfile to your own Docker Hub account.