Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
403886e
build(pom): share test dependencies and expose the common test-jar
EdgarEldy Sep 19, 2026
b61861d
build(core): declare the auth, catalog, customer and order modules
EdgarEldy Sep 19, 2026
a01445c
build(common): add the JDBC, Flyway and test-jar dependencies
EdgarEldy Sep 19, 2026
c9178aa
build(ws): add Spring MVC, Jackson, validation and logging dependencies
EdgarEldy Sep 19, 2026
91c8265
feat(common): add ResourceNotFoundException and BusinessRuleException
EdgarEldy Sep 19, 2026
b7f207a
feat(common): add the Flyway V1 schema for both domains
EdgarEldy Sep 19, 2026
5fb25ac
feat(common): add AbstractDao with shared JdbcTemplate helpers
EdgarEldy Sep 19, 2026
bdcf0e4
feat(common): declare DataSource, Flyway, JdbcTemplate and transactions
EdgarEldy Sep 19, 2026
fbb1d84
feat(common): add a health check through the five-layer split
EdgarEldy Sep 19, 2026
8fb2304
feat(ws): add ApiResponse, PageResponse and HealthResponse payloads
EdgarEldy Sep 19, 2026
bb048a7
feat(ws): map every exception to an ApiResponse in GlobalExceptionHan…
EdgarEldy Sep 19, 2026
8739721
feat(ws): add the public health route
EdgarEldy Sep 19, 2026
9aace48
feat(ws): configure Spring MVC explicitly with WebMvcConfig
EdgarEldy Sep 19, 2026
a7c5f6b
feat(ws): replace web.xml with a WebApplicationInitializer
EdgarEldy Sep 19, 2026
b7526cc
test(common): verify the Flyway schema against a real PostgreSQL
EdgarEldy Sep 19, 2026
a360c64
test(common): check that DataSourceConfig fails without its settings
EdgarEldy Sep 19, 2026
fee8885
test(common): cover AbstractDao and HealthDao against PostgreSQL
EdgarEldy Sep 19, 2026
ce291f6
test(common): unit test HealthServiceImpl with a mocked DAO
EdgarEldy Sep 19, 2026
6a85553
test(ws): cover ApiResponse and PageResponse
EdgarEldy Sep 19, 2026
177723b
test(ws): verify HTTP error mapping and the health controller with Mo…
EdgarEldy Sep 19, 2026
c3bf50a
test(ws): boot the real WebMvcConfig against PostgreSQL
EdgarEldy Sep 19, 2026
b8a2051
build(docker): add a multi-stage Dockerfile for the WAR on Tomcat 10.1
EdgarEldy Sep 19, 2026
78173a6
build(docker): add docker-compose for the WAR and PostgreSQL 16
EdgarEldy Sep 19, 2026
351b854
ci(actions): run mvn verify on the whole reactor
EdgarEldy Sep 19, 2026
60dd8f7
ci(actions): lint commit messages against Conventional Commits
EdgarEldy Sep 19, 2026
63b93ca
docs(github): add the pull request template
EdgarEldy Sep 19, 2026
8619f3d
docs(readme): tick core-architecture tasks and note two deviations
EdgarEldy Sep 19, 2026
8122095
fix(ws): stop the catch-all from leaking ErrorResponse detail text
EdgarEldy Sep 19, 2026
31f4f3c
fix(ws): register the validator as a bean and keep default converters
EdgarEldy Sep 19, 2026
9473c7c
fix(common): log the cause when the health check fails
EdgarEldy Sep 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Keep the build context small and free of local files (keys, env files, git data)
*
!pom.xml
!core/
!ws/
!mvnw
!.mvn/
**/target/
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copy to .env (ignored by git) and adjust. Read by docker-compose.yml only.
DB_NAME=spring_jdbc_tutorial
DB_USER=spring
DB_PASSWORD=change-me
APP_PORT=8080
32 changes: 32 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Branch

<!-- e.g. feature/core-architecture -->

## Task checklist

<!-- Copy the relevant section's checklist from README.md and check off each item. -->

- [ ]

## Commit summary

<!-- One line per group of commits, summarizing what changed and why. -->

## Test plan

- [ ] Tests exist at every layer the branch's checklist calls for (DAO, RowMapper, EntityMapper, service, controller)
- [ ] Each DAO test class loads its own dedicated SQL fixture file
- [ ] `./mvnw clean verify` is green on the whole reactor

## Code review checklist

- [ ] No business logic in controllers; controllers only see services and `payload/` types
- [ ] DAOs only use `entity/` types, services only `dto/` types at their boundary
- [ ] Every response, success or error, is an `ApiResponse<T>` (errors through `GlobalExceptionHandler`, 401/403 through the security handlers)
- [ ] Permission checks are declarative (`@PreAuthorize("hasPermission(...)")`), no manual permission `if`
- [ ] No `@Repository`, `@Service` or `@Component` on a DAO or service; every bean is an explicit `@Bean` in `DaoConfig`/`ServiceConfig`
- [ ] No Lombok, no `record`, no ORM (`JdbcTemplate` only)
- [ ] Every RBAC mutation calls `AuditLogger`
- [ ] The database schema only changes through a Flyway migration
- [ ] Test methods are named `_NN_Should<Outcome>_When<Condition>`
- [ ] Commits are atomic, follow Conventional Commits with a scope, no em dash, no tool attribution
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: maven

# No "services: postgres" block on purpose. The DAO tests start their own PostgreSQL
# container through Testcontainers and the Docker daemon already available on
# ubuntu-latest, exactly as they do locally, so nothing is provisioned by the workflow.
- name: Build and test the whole reactor
run: ./mvnw -B verify
43 changes: 43 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: PR checks

on:
pull_request:
branches: [master, develop]

jobs:
commit-lint:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Validate commit messages (Conventional Commits)
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail

pattern='^(feat|fix|refactor|test|docs|chore|build|ci|style|perf)\([a-z0-9.,/_-]+\): .{1,100}$'
failed=0

while IFS= read -r subject; do
[ -z "$subject" ] && continue
if [[ "$subject" =~ $pattern ]]; then
echo "OK: $subject"
else
echo "INVALID: $subject"
failed=1
fi
done < <(git log "$BASE_SHA..$HEAD_SHA" --no-merges --pretty=format:'%s')

if [ "$failed" -ne 0 ]; then
echo ""
echo "One or more commit messages do not follow Conventional Commits."
echo "Expected format: <type>(<scope>): <short summary>"
echo "Allowed types: feat, fix, refactor, test, docs, chore, build, ci, style, perf"
exit 1
fi
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Multi-stage image for the packaged WAR.
# Stage 1 compiles and packages with Maven, stage 2 is a Tomcat 10.1 runtime holding only the WAR.

FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /build

# Copy the poms first so Docker caches the dependency download until a pom changes.
COPY pom.xml ./
COPY core/pom.xml core/pom.xml
COPY core/common/pom.xml core/common/pom.xml
COPY core/auth/pom.xml core/auth/pom.xml
COPY core/catalog/pom.xml core/catalog/pom.xml
COPY core/customer/pom.xml core/customer/pom.xml
COPY core/order/pom.xml core/order/pom.xml
COPY ws/pom.xml ws/pom.xml
RUN mvn -B -q dependency:go-offline -DskipTests || true

COPY core ./core
COPY ws ./ws
# Tests are skipped here: the DAO tests need Testcontainers, i.e. a Docker daemon that a build
# stage does not have. They run in CI with mvn verify instead.
RUN mvn -B -q package -DskipTests

FROM tomcat:10.1-jre17-temurin
# The README documents the API under /spring-jdbc-tutorial/api/v1: the WAR name is the context path.
RUN rm -rf /usr/local/tomcat/webapps/*
COPY --from=build /build/ws/target/ws.war /usr/local/tomcat/webapps/spring-jdbc-tutorial.war

# Run as a non-root user so a compromised process has no rights on the container.
RUN useradd --system --no-create-home spring \
&& chown -R spring /usr/local/tomcat/webapps /usr/local/tomcat/logs /usr/local/tomcat/temp /usr/local/tomcat/work /usr/local/tomcat/conf
USER spring

EXPOSE 8080
CMD ["catalina.sh", "run"]
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,9 @@ spring-jdbc-tutorial/
│ │ in this project, since Spring MVC requires it for this mechanism)
│ └── config/
│ ├── WebAppInitializer.java (WebApplicationInitializer, replaces web.xml)
│ ├── WebMvcConfig.java (@EnableWebMvc, imports every core module's DaoConfig/ServiceConfig)
│ ├── WebMvcConfig.java (@EnableWebMvc, imports every core module's DaoConfig/ServiceConfig, scans only ws.controller and ws.exception)
│ ├── SecurityConfig.java (SecurityFilterChain, method security enabling @PreAuthorize)
│ └── OpenApiConfig.java
│ └── OpenApiConfig.java (not delivered: springdoc-openapi relies on Spring Boot classes)
└── webapp/
└── (empty - no JSPs, API-only)
├── docker-compose.yml
Expand Down Expand Up @@ -310,13 +310,13 @@ No other naming style (`shouldX()`, `testX()`, `givenX_whenY_thenZ()`) is used a

### Tasks

- [ ] Parent `pom.xml` (packaging `pom`, modules `core`/`ws`, `dependencyManagement` for Spring Framework 6.2.x, Testcontainers, etc.)
- [ ] `core/pom.xml` (packaging `pom`, modules `common`/`auth`/`catalog`/`customer`/`order`)
- [ ] `core/common`: `ResourceNotFoundException`, `BusinessRuleException`, `AbstractDao` (holds a `JdbcTemplate` reference, offers a couple of shared helpers), `DataSourceConfig` (`DataSource` via HikariCP, `JdbcTemplate`, `DataSourceTransactionManager`)
- [ ] Flyway script `V1__init_schema.sql` (all tables from both domains, including `audit_logs`)
- [ ] `ws` skeleton: `WebAppInitializer` (`WebApplicationInitializer`), `WebMvcConfig` (`@EnableWebMvc`, no module config imported yet), `ApiResponse<T>`, `PageResponse<T>`, `GlobalExceptionHandler`
- [ ] `docker-compose.yml` (`ws` deployed on Tomcat + PostgreSQL), `Dockerfile` (multi-stage: Maven build, Tomcat 10.1 runtime)
- [ ] `.github/workflows/ci.yml`: `mvn verify` across the whole reactor
- [x] Parent `pom.xml` (packaging `pom`, modules `core`/`ws`, `dependencyManagement` for Spring Framework 6.2.x, Testcontainers, etc.)
- [x] `core/pom.xml` (packaging `pom`, modules `common`/`auth`/`catalog`/`customer`/`order`)
- [x] `core/common`: `ResourceNotFoundException`, `BusinessRuleException`, `AbstractDao` (holds a `JdbcTemplate` reference, offers a couple of shared helpers), `DataSourceConfig` (`DataSource` via HikariCP, `JdbcTemplate`, `DataSourceTransactionManager`)
- [x] Flyway script `V1__init_schema.sql` (all tables from both domains, including `audit_logs`)
- [x] `ws` skeleton: `WebAppInitializer` (`WebApplicationInitializer`), `WebMvcConfig` (`@EnableWebMvc`, no module config imported yet), `ApiResponse<T>`, `PageResponse<T>`, `GlobalExceptionHandler`
- [x] `docker-compose.yml` (`ws` deployed on Tomcat + PostgreSQL), `Dockerfile` (multi-stage: Maven build, Tomcat 10.1 runtime)
- [x] `.github/workflows/ci.yml`: `mvn verify` across the whole reactor

## feature/auth

Expand Down
23 changes: 23 additions & 0 deletions core/auth/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.edgareldy.springjdbctutorial</groupId>
<artifactId>core</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>

<artifactId>auth</artifactId>
<name>auth</name>
<description>Identity and RBAC domain: users, roles, permissions, tokens, audit logs</description>

<dependencies>
<dependency>
<groupId>com.edgareldy.springjdbctutorial</groupId>
<artifactId>common</artifactId>
</dependency>
</dependencies>
</project>
23 changes: 23 additions & 0 deletions core/catalog/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.edgareldy.springjdbctutorial</groupId>
<artifactId>core</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>

<artifactId>catalog</artifactId>
<name>catalog</name>
<description>E-commerce catalog: categories and products</description>

<dependencies>
<dependency>
<groupId>com.edgareldy.springjdbctutorial</groupId>
<artifactId>common</artifactId>
</dependency>
</dependencies>
</project>
57 changes: 57 additions & 0 deletions core/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,66 @@
<description>Shared exceptions, AbstractDao and DataSourceConfig</description>

<dependencies>
<!-- JdbcTemplate, DataSourceTransactionManager, @Transactional support -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<!-- @Configuration, @Bean, Environment -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<!-- Annotations only (@Table, @Id, @Column) used as documentation on entity classes.
Every core module gets them transitively through common. -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<!-- Publishes the test classes (Testcontainers PostgreSQL support) as a test-jar
so the DAO tests of the other core modules can reuse them -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.edgareldy.springjdbctutorial.core.common.config;

import com.edgareldy.springjdbctutorial.core.common.dao.HealthDao;
import com.edgareldy.springjdbctutorial.core.common.dao.impl.HealthDaoImpl;
import com.edgareldy.springjdbctutorial.core.common.service.HealthService;
import com.edgareldy.springjdbctutorial.core.common.service.impl.HealthServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;

/**
* Wiring of the common module: imports the infrastructure and declares the health DAO and service.
* <p>
* Created edgar.muhamyangabo on 9/19/26
* Author : edgar.muhamyangabo
* Date : 9/19/26
* Project : spring-jdbc-tutorial
*/
// @Import pulls DataSourceConfig into whichever context imports this class, so a module config only
// has to import what it directly needs. Beans are typed by their interface: callers never see the
// implementation class.
@Configuration
@Import(DataSourceConfig.class)
public class CommonConfig {

@Bean
public HealthDao healthDao(JdbcTemplate jdbcTemplate) {
return new HealthDaoImpl(jdbcTemplate);
}

@Bean
public HealthService healthService(HealthDao healthDao) {
return new HealthServiceImpl(healthDao);
}
}
Loading
Loading