A comprehensive Spring Boot 4 application for tracking the entire plant lifecycle from germination to fruit/flower. Built with Java 25, PostgreSQL, Flyway, and Docker.
src/main/java/com/jrafo/plantlifecycle/
├── PlantLifecycleApplication.java
├── config/ # Security, JWT, OpenAPI configuration
├── controller/ # REST API controllers
├── dto/ # Request/response records
├── exception/ # Global error handling (RFC 7807)
├── mapper/ # Entity ↔ DTO mappers
├── model/ # JPA entities and enums
├── repository/ # Spring Data JPA repositories
└── service/ # Business logic and metrics
Entity
Purpose
Strain
Cultivar metadata, genetics, ideal environmental ranges
Batch
Groups plants from the same strain and sowing date
Plant
Individual plant tracked through lifecycle stages
Stage
Lifecycle phase with start/end timestamps
EnvironmentReading
Timestamped sensor data (temp, humidity, CO2, etc.)
Sensor
Physical sensor with calibration and location info
Location
Growing room/area
Event
Significant lifecycle events (watering, feeding, etc.)
User
Basic auth user for ownership and access
SEED → GERMINATION → SEEDLING → VEGETATIVE → FLOWERING → FRUITING → HARVEST → DRYING → CURING → COMPLETED
Java 25 (JDK)
Maven 4 RC5 (or use the included wrapper)
Docker and Docker Compose
git clone https://github.com/rafodelmal/jrafo.git
cd jrafo
cp .env.example .env
2. Start with Docker Compose
# Start PostgreSQL only (for local dev)
docker compose up -d postgres
# Or start everything (app + database)
docker compose up -d --build
3. Run locally (without Docker for the app)
# Make sure Postgres is running (via docker compose or locally)
docker compose up -d postgres
# Build and run
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
The application starts on http://localhost:8080 .
Open http://localhost:8080/swagger-ui.html for the interactive API documentation.
# Compile
./mvnw compile
# Run unit tests
./mvnw test
# Run integration tests (requires Docker for Testcontainers)
./mvnw verify
# Package
./mvnw package -DskipTests
Method
Endpoint
Description
POST
/api/auth/register
Register a new user
POST
/api/auth/login
Login and get JWT
Method
Endpoint
Description
GET
/api/strains
List all strains
GET
/api/strains/{id}
Get strain by ID
POST
/api/strains
Create a strain
PUT
/api/strains/{id}
Update a strain
DELETE
/api/strains/{id}
Delete a strain
Method
Endpoint
Description
GET
/api/plants
List all plants
GET
/api/plants?batchId={id}
Filter by batch
GET
/api/plants/{id}
Get plant by ID
POST
/api/plants
Create a plant
DELETE
/api/plants/{id}
Delete a plant
Method
Endpoint
Description
GET
/api/batches
List all batches
GET
/api/batches/{id}
Get batch by ID
POST
/api/batches
Create a batch
DELETE
/api/batches/{id}
Delete a batch
Method
Endpoint
Description
GET
/api/sensors
List all sensors
GET
/api/sensors/{id}
Get sensor by ID
POST
/api/sensors
Register a sensor
DELETE
/api/sensors/{id}
Delete a sensor
Method
Endpoint
Description
POST
/api/readings
Ingest single reading
POST
/api/readings/bulk
Ingest bulk readings
GET
/api/readings?plantId={id}&from=...&to=...
Query by plant + date range
GET
/api/readings?batchId={id}&from=...&to=...
Query by batch + date range
GET
/api/readings?sensorId={id}&from=...&to=..
Query by sensor + date range
Method
Endpoint
Description
POST
/api/stages
Transition to a new stage
GET
/api/stages/plant/{plantId}
Get all stages for a plant
Method
Endpoint
Description
GET
/api/reports/plants/{plantId}/summary
Lifecycle summary for a plant
Endpoint
Description
/actuator/health
Health check
/actuator/prometheus
Prometheus metrics
/actuator/info
Application info
The seed data (Flyway V2) pre-populates sample strains, a batch, a plant, and readings. Here's how to interact with the API from scratch:
Step 1: Register and login
# Register
curl -s -X POST http://localhost:8080/api/auth/register \
-H " Content-Type: application/json" \
-d ' {
"username": "grower1",
"password": "securepass123",
"email": "grower1@example.com",
"fullName": "Green Thumb"
}'
# Login (get JWT)
TOKEN=$( curl -s -X POST http://localhost:8080/api/auth/login \
-H " Content-Type: application/json" \
-d ' {"username": "grower1", "password": "securepass123"}' | jq -r ' .token' )
curl -s -X POST http://localhost:8080/api/strains \
-H " Content-Type: application/json" \
-H " Authorization: Bearer $TOKEN " \
-d ' {
"name": "San Marzano Tomato",
"genetics": "heirloom",
"origin": "Italy",
"description": "Classic Italian paste tomato",
"expectedGerminationDays": 7,
"expectedSeedlingDays": 21,
"expectedVegetativeDays": 35,
"expectedFloweringDays": 14,
"expectedFruitingDays": 60,
"idealTempMinCelsius": 18.0,
"idealTempMaxCelsius": 30.0,
"idealHumidityMinPercent": 45.0,
"idealHumidityMaxPercent": 75.0,
"idealPhMin": 6.0,
"idealPhMax": 6.8,
"idealEcMinMsCm": 2.0,
"idealEcMaxMsCm": 5.0,
"idealCo2MinPpm": 400.0,
"idealCo2MaxPpm": 1200.0,
"idealLightMinPpfd": 200.0,
"idealLightMaxPpfd": 800.0,
"idealSoilMoistureMinPercent": 40.0,
"idealSoilMoistureMaxPercent": 70.0
}' | jq .
Expected response:
{
"id" : " <uuid>" ,
"name" : " San Marzano Tomato" ,
"genetics" : " heirloom" ,
"origin" : " Italy" ,
"idealTempMinCelsius" : 18.0 ,
"idealTempMaxCelsius" : 30.0 ,
"createdAt" : " 2026-02-06T..." ,
"updatedAt" : " 2026-02-06T..."
}
STRAIN_ID=" <strain-id-from-above>"
curl -s -X POST http://localhost:8080/api/batches \
-H " Content-Type: application/json" \
-H " Authorization: Bearer $TOKEN " \
-d " {
\" name\" : \" Spring 2026 San Marzano\" ,
\" strainId\" : \" $STRAIN_ID \" ,
\" sowDate\" : \" 2026-02-06\" ,
\" plantCount\" : 4,
\" notes\" : \" Started indoors under grow lights\"
}" | jq .
BATCH_ID=" <batch-id-from-above>"
curl -s -X POST http://localhost:8080/api/plants \
-H " Content-Type: application/json" \
-H " Authorization: Bearer $TOKEN " \
-d " {
\" name\" : \" SM Plant #1\" ,
\" strainId\" : \" $STRAIN_ID \" ,
\" batchId\" : \" $BATCH_ID \" ,
\" sowDate\" : \" 2026-02-06\"
}" | jq .
PLANT_ID=" <plant-id-from-above>"
curl -s -X POST http://localhost:8080/api/stages \
-H " Content-Type: application/json" \
-H " Authorization: Bearer $TOKEN " \
-d " {
\" plantId\" : \" $PLANT_ID \" ,
\" phase\" : \" GERMINATION\" ,
\" notes\" : \" Radicle emerged\"
}" | jq .
Step 6: Ingest bulk environment readings
curl -s -X POST http://localhost:8080/api/readings/bulk \
-H " Content-Type: application/json" \
-H " Authorization: Bearer $TOKEN " \
-d " {
\" readings\" : [
{
\" recordedAt\" : \" 2026-02-06T08:00:00Z\" ,
\" plantId\" : \" $PLANT_ID \" ,
\" batchId\" : \" $BATCH_ID \" ,
\" temperatureCelsius\" : 24.5,
\" humidityPercent\" : 62.0,
\" co2Ppm\" : 800,
\" lightPpfd\" : 350,
\" ph\" : 6.3,
\" ecMsCm\" : 2.5,
\" soilMoisturePercent\" : 55.0
},
{
\" recordedAt\" : \" 2026-02-06T14:00:00Z\" ,
\" plantId\" : \" $PLANT_ID \" ,
\" batchId\" : \" $BATCH_ID \" ,
\" temperatureCelsius\" : 27.0,
\" humidityPercent\" : 55.0,
\" co2Ppm\" : 900,
\" lightPpfd\" : 500,
\" ph\" : 6.5,
\" ecMsCm\" : 2.8,
\" soilMoisturePercent\" : 48.0
},
{
\" recordedAt\" : \" 2026-02-06T20:00:00Z\" ,
\" plantId\" : \" $PLANT_ID \" ,
\" batchId\" : \" $BATCH_ID \" ,
\" temperatureCelsius\" : 21.0,
\" humidityPercent\" : 68.0,
\" co2Ppm\" : 750,
\" lightPpfd\" : 0,
\" ph\" : 6.2,
\" ecMsCm\" : 2.3,
\" soilMoisturePercent\" : 60.0
}
]
}" | jq .
Step 7: Fetch lifecycle summary
curl -s http://localhost:8080/api/reports/plants/$PLANT_ID /summary | jq .
Expected response:
{
"plantId" : " <uuid>" ,
"plantName" : " SM Plant #1" ,
"strainName" : " San Marzano Tomato" ,
"currentPhase" : " GERMINATION" ,
"sowDate" : " 2026-02-06" ,
"active" : true ,
"stages" : [
{
"phase" : " GERMINATION" ,
"startedAt" : " 2026-02-06T..." ,
"notes" : " Radicle emerged"
}
],
"overallMetrics" : {
"avgTemperature" : 24.17 ,
"avgHumidity" : 61.67 ,
"avgCo2" : 816.67 ,
"avgLight" : 283.33 ,
"avgPh" : 6.33 ,
"avgEc" : 2.53 ,
"avgSoilMoisture" : 54.33 ,
"minTemperature" : 21.0 ,
"maxTemperature" : 27.0 ,
"readingCount" : 3 ,
"alerts" : []
},
"recentEvents" : [
{
"eventType" : " STAGE_TRANSITION" ,
"title" : " Stage transition to GERMINATION"
}
],
"generatedAt" : " 2026-02-06T..."
}
Using the pre-seeded data
The seed data includes a ready-to-use plant. Fetch the summary for it:
curl -s http://localhost:8080/api/reports/plants/e0000000-0000-0000-0000-000000000001/summary | jq .
Variable
Default
Description
POSTGRES_HOST
localhost
Database host
POSTGRES_PORT
5432
Database port
POSTGRES_DB
plantlifecycle
Database name
POSTGRES_USER
plantuser
Database user
POSTGRES_PASSWORD
plantpass
Database password
JWT_SECRET
(see .env.example)
JWT signing key
JWT_EXPIRATION_MS
86400000
JWT TTL (24h)
SPRING_PROFILES_ACTIVE
dev
Spring profile
SERVER_PORT
8080
Application port
default : Production-ready settings
dev : Debug logging, SQL output enabled
jrafo/
├── pom.xml # Maven 4 RC5 configuration
├── Dockerfile # Multi-stage build
├── docker-compose.yml # Postgres + App services
├── .env.example # Environment template
├── .mvn/
│ ├── maven.config
│ └── wrapper/maven-wrapper.properties
├── src/main/
│ ├── java/com/jrafo/plantlifecycle/
│ │ ├── config/ # Security, JWT, OpenAPI
│ │ ├── controller/ # REST controllers
│ │ ├── dto/ # Request/Response records
│ │ ├── exception/ # Error handling
│ │ ├── mapper/ # Entity ↔ DTO mappers
│ │ ├── model/ # JPA entities + enums
│ │ ├── repository/ # Spring Data repos
│ │ └── service/ # Business logic
│ └── resources/
│ ├── application.yml
│ ├── application-dev.yml
│ └── db/migration/
│ ├── V1__create_schema.sql
│ └── V2__seed_data.sql
└── src/test/
└── java/com/jrafo/plantlifecycle/
├── integration/ # Full-stack integration tests
├── mapper/ # Mapper unit tests
├── repository/ # Repository integration tests
└── service/ # Service unit tests
MIT
Java Software Rules by Rafo