Project layout (dev-first)¶
1) Project tree (dev-first)¶
my-service/
ββ build.gradle / pom.xml
ββ settings.gradle
ββ .gitignore
ββ README.md
ββ .env # local-only env vars for compose/scripts (never commit secrets)
β
ββ src/
β ββ main/
β β ββ java/... # app code
β β ββ resources/
β β β ββ application.yml # shared defaults (safe to commit)
β β β ββ application-dev.yml # dev profile overrides (safe to commit)
β β β ββ logback-spring.xml # logging config: console + file (to ./var/log)
β ββ test/java/... # tests
β
ββ config/ # **externalized config** mounted/loaded in dev
β ββ application-local.yml # machine-specific overrides; not committed
β ββ secrets.example.yml # example only (commit), copy to secrets.yml locally
β
ββ var/ # dev runtime artifacts (git-ignored)
β ββ log/ # app logs in dev
β β ββ app.log
β ββ data/ # app writable data (e.g., file uploads)
β ββ tmp/ # ephemeral scratch files
β
ββ db/ # dev database assets
β ββ migrations/ # Flyway/Liquibase scripts (commit)
β ββ data/ # local DB files or mounted volume (git-ignored)
β
ββ docker/
β ββ Dockerfile
β ββ compose.yml # local stack (app + postgres + pgadmin, etc.)
β
ββ scripts/
ββ run-dev.sh # runs app with dev profile + external config
ββ format.sh
ββ wait-for-it.sh
Why this layout?¶
src/main/resources/application.ymlholds safe defaults (commit).application-dev.ymlcontains dev profile overrides youβre happy to commit (e.g., use H2 or a local Postgres, verbose logging).config/is for externalized, machine-specific config (donβt commit secrets). Load viaSPRING_CONFIG_ADDITIONAL_LOCATION.var/log,var/data,var/tmpkeep runtime files out of your code and are git-ignored.db/migrationsis versioned;db/datais not (local volumes only).
2) Minimal .gitignore¶
# Build
/target/
/build/
/out/
# IDE
.idea/
.project
.classpath
.settings/
*.iml
# Runtime (dev)
/var/
/config/application-local.yml
/config/secrets.yml
/db/data/
/*.log
# OS cruft
.DS_Store
Thumbs.db
3) Spring config: profiles + externalized location¶
src/main/resources/application.yml (safe defaults)
spring:
application:
name: my-service
datasource:
url: jdbc:postgresql://localhost:5432/my_service
username: my_service
password: changeme # defaults for dev only; override via external config or env
jpa:
hibernate:
ddl-auto: validate
flyway:
locations: classpath:db/migration
logging:
file:
name: var/log/app.log # relative to project root when run from there
level:
root: INFO
server:
port: 8080
src/main/resources/application-dev.yml
spring:
config:
activate:
on-profile: dev
jpa:
show-sql: true
logging:
level:
org.springframework: DEBUG
External (not committed): config/application-local.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/my_service
username: my_service
password: supersecret # local only
Load it in dev with either:
- Env var:
SPRING_PROFILES_ACTIVE=devSPRING_CONFIG_ADDITIONAL_LOCATION=./config/ - Or via CLI:
./gradlew bootRun --args='--spring.profiles.active=dev --spring.config.additional-location=./config/'
4) Logging to both console and file¶
src/main/resources/logback-spring.xml
<configuration scan="true">
<property name="LOG_FILE" value="var/log/app.log"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>var/log/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
5) Docker Compose for local stack¶
docker/compose.yml
version: "3.9"
services:
db:
image: postgres:16
container_name: my_service_db
environment:
POSTGRES_DB: my_service
POSTGRES_USER: my_service
POSTGRES_PASSWORD: supersecret
volumes:
- ../db/data:/var/lib/postgresql/data
ports:
- "5432:5432"
app:
build:
context: ..
dockerfile: docker/Dockerfile
environment:
SPRING_PROFILES_ACTIVE: dev
SPRING_CONFIG_ADDITIONAL_LOCATION: /workspace/config/
volumes:
- ..:/workspace # mount code (hot reload with DevTools)
- ../var/log:/workspace/var/log
- ../config:/workspace/config
ports:
- "8080:8080"
depends_on:
- db
6) Run scripts¶
scripts/run-dev.sh
#!/usr/bin/env bash
set -euo pipefail
export SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-dev}
export SPRING_CONFIG_ADDITIONAL_LOCATION=${SPRING_CONFIG_ADDITIONAL_LOCATION:-./config/}
# Gradle
./gradlew bootRun \
--args="--spring.config.additional-location=${SPRING_CONFIG_ADDITIONAL_LOCATION} --spring.profiles.active=${SPRING_PROFILES_ACTIVE}"
7) Gradle/Maven snippets¶
Gradle (Kotlin DSL) β add Spring Boot DevTools for hot reload
dependencies {
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("org.postgresql:postgresql")
implementation("org.flywaydb:flyway-core")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Maven
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
</dependencies>
8) Conventions & tips¶
- Never commit anything in
var/,config/application-local.yml, ordb/data/. - Keep sane defaults in
application.yml; put dev overrides inapplication-dev.yml; put machine secrets inconfig/application-local.yml. - Point file-based resources (uploads, temp exports) to
var/dataandvar/tmp. - If you need multiple services later, keep this shape per service and add a
/infrarepo or a root-levelcompose.ymlthat wires them together.