A Java Spring Boot application that acts as an object store for text data, with REST API endpoints for reading and writing data to a PostgreSQL database.
Quick Start:
- Local Development: See Running the Application section below
- Docker Deployment: See DOCKER.md for complete Docker setup instructions
- RESTful API for storing and retrieving text objects
- PostgreSQL database backend
- Automatic database schema creation
- Unique key-based storage with automatic updates
- Timestamp tracking (created_at, updated_at)
- Prometheus metrics for monitoring (endpoint latency, request counts, business metrics)
- Comprehensive logging with configurable log levels
- Java 11 or higher
- Maven 3.6+
- PostgreSQL 14+ (or compatible version)
- macOS, Linux, or Windows
See DOCKER.md for Docker prerequisites and setup instructions.
# Install PostgreSQL 14
brew install postgresql@14
# Start PostgreSQL service (will auto-start on login)
brew services start postgresql@14# Update package list
sudo apt update
# Install PostgreSQL
sudo apt install postgresql postgresql-contrib
# Start PostgreSQL service
sudo systemctl start postgresql
sudo systemctl enable postgresqlDownload and install PostgreSQL from https://www.postgresql.org/download/windows/
-
Connect to PostgreSQL (as your default user):
# macOS (Homebrew) /opt/homebrew/opt/postgresql@14/bin/psql -d postgres # Linux sudo -u postgres psql # Windows (use psql from PostgreSQL installation) psql -U postgres
-
Create the database:
CREATE DATABASE objectstore;
-
Create the user (if not using default postgres user):
CREATE USER postgres WITH PASSWORD 'postgres' SUPERUSER; GRANT ALL PRIVILEGES ON DATABASE objectstore TO postgres;
-
Verify the database:
\l
You should see
objectstorein the list.
The application is configured to connect to PostgreSQL with the following default settings (in src/main/resources/application.properties):
spring.datasource.url=jdbc:postgresql://localhost:5432/objectstore
spring.datasource.username=postgres
spring.datasource.password=postgresTo change these settings, edit src/main/resources/application.properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/your_database
spring.datasource.username=your_username
spring.datasource.password=your_password-
Navigate to the project directory:
cd /path/to/MercorProj -
Ensure PostgreSQL is running:
# macOS (Homebrew) brew services list | grep postgresql@14 # Linux sudo systemctl status postgresql
-
Run the application:
mvn spring-boot:run
The application will start on
http://localhost:8080
-
Build the application:
mvn clean package
-
Run the JAR:
java -jar target/MercorProj-1.0-SNAPSHOT.jar
- Open the project in your IDE (IntelliJ IDEA, Eclipse, etc.)
- Run the
ObjectStoreApplicationclass - The application will start automatically
For Docker deployment instructions, including Docker Compose setup, building images, running containers, environment variables, and all Docker-related commands, see DOCKER.md.
The application supports different log levels (ERROR, WARN, INFO, DEBUG, TRACE). You can configure logging in several ways:
Set the LOG_LEVEL environment variable before starting the application:
# Set log level to DEBUG
export LOG_LEVEL=DEBUG
mvn spring-boot:run
# Or set it inline
LOG_LEVEL=DEBUG mvn spring-boot:run
# Set log level to ERROR (less verbose)
LOG_LEVEL=ERROR mvn spring-boot:runPass the log level as a system property:
# Using Maven
mvn spring-boot:run -Dlogging.level.root=DEBUG
# Using JAR
java -jar target/MercorProj-1.0-SNAPSHOT.jar --logging.level.root=DEBUG
# Set application-specific log level
java -jar target/MercorProj-1.0-SNAPSHOT.jar --logging.level.com.mercor.objectstore=DEBUGEdit src/main/resources/application.properties:
# Set root log level
logging.level.root=DEBUG
# Set application-specific log level
logging.level.com.mercor.objectstore=DEBUG- ERROR: Only error messages (highest priority)
- WARN: Warning and error messages
- INFO: Informational, warning, and error messages (default)
- DEBUG: Detailed debugging information
- TRACE: Very detailed tracing information (most verbose)
# Production mode (minimal logging)
LOG_LEVEL=WARN mvn spring-boot:run
# Development mode (detailed logging)
LOG_LEVEL=DEBUG mvn spring-boot:run
# Troubleshooting mode (very detailed)
LOG_LEVEL=TRACE mvn spring-boot:runLogs are output to the console by default. The log format includes:
- Timestamp
- Thread name
- Log level
- Logger name
- Message
Example log output:
2026-01-02 15:55:32.123 [main] INFO c.m.o.ObjectStoreApplication - Starting Object Store Application...
2026-01-02 15:55:32.456 [main] INFO c.m.o.c.TextObjectController - TextObjectController initialized
2026-01-02 15:55:33.789 [http-nio-8080-exec-1] INFO c.m.o.c.TextObjectController - Creating or updating object with key=test-key
Once the application is running, you should see output similar to:
Started ObjectStoreApplication in X.XXX seconds
Tomcat started on port(s): 8080 (http)
The database table text_objects will be automatically created on first startup.
The project includes comprehensive unit tests for both the service and controller layers. The tests use JUnit 5, Mockito, and Spring Boot Test.
To run all tests in the project:
mvn testThis will:
- Compile the test code
- Run all unit tests
- Display a summary of test results
Expected output:
Tests run: 24, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
You can run individual test classes:
# Run service layer tests only
mvn test -Dtest=TextObjectServiceTest
# Run controller layer tests only
mvn test -Dtest=TextObjectControllerTestTo run a specific test method within a test class:
mvn test -Dtest=TextObjectServiceTest#testSave_NewObjectThe project includes the following test suites:
- Tests for creating new objects
- Tests for updating existing objects
- Tests for retrieving objects by key
- Tests for listing all objects
- Tests for checking object existence
- Tests for deleting objects
- Edge cases (empty keys, null content)
- REST API endpoint tests using MockMvc
- Request validation tests
- Success and error response tests
- HTTP status code verification
- JSON response validation
Most IDEs provide built-in test runners:
IntelliJ IDEA:
- Right-click on a test class or method
- Select "Run 'TestName'"
- Or use the green play button next to test methods
Eclipse:
- Right-click on a test class
- Select "Run As" → "JUnit Test"
When tests run successfully, you'll see output like:
[INFO] Running com.mercor.objectstore.service.TextObjectServiceTest
[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
[INFO] Running com.mercor.objectstore.controller.TextObjectControllerTest
[INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] Results:
[INFO] Tests run: 24, Failures: 0, Errors: 0, Skipped: 0
If you need to skip tests during the build process:
# Skip tests when packaging
mvn clean package -DskipTests
# Skip tests and compilation
mvn clean package -Dmaven.test.skip=trueNote: It's recommended to run tests before committing code to ensure everything works correctly.
http://localhost:8080/api/objects
The service provides multiple health check endpoints for monitoring and status verification.
GET /health
Returns a comprehensive health status including database connectivity.
Response (200 OK when healthy):
{
"status": "UP",
"service": "Object Store API",
"database": "UP"
}Response (503 Service Unavailable when unhealthy):
{
"status": "DOWN",
"service": "Object Store API",
"database": "DOWN",
"databaseError": "Connection refused"
}Example using curl:
curl http://localhost:8080/healthGET /health/simple
Returns a simple status check without database verification.
Response (200 OK):
{
"status": "UP"
}Example using curl:
curl http://localhost:8080/health/simpleGET /actuator/health
Spring Boot Actuator's built-in health endpoint.
Response (200 OK):
{
"status": "UP"
}Example using curl:
curl http://localhost:8080/actuator/healthNote: The health check endpoints are useful for:
- Monitoring tools and load balancers
- Kubernetes liveness/readiness probes
- CI/CD pipeline health checks
- Quick service status verification
GET /actuator/prometheus
Exposes application metrics in Prometheus format for monitoring and alerting.
Response (200 OK):
# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{application="objectstore",method="POST",status="200",uri="/api/objects"} 5.0
# HELP http_request_duration_seconds HTTP request latency
# TYPE http_request_duration_seconds summary
http_request_duration_seconds{application="objectstore",quantile="0.5"} 0.023
http_request_duration_seconds{application="objectstore",quantile="0.95"} 0.045
http_request_duration_seconds{application="objectstore",quantile="0.99"} 0.089
# HELP objectstore_objects_created_total Total number of objects created
# TYPE objectstore_objects_created_total counter
objectstore_objects_created_total{application="objectstore"} 10.0
Example using curl:
curl http://localhost:8080/actuator/prometheusAvailable Metrics:
-
HTTP Metrics:
http_requests_total- Total number of HTTP requests (tagged by method, URI, status)http_request_duration_seconds- Request latency (with percentiles: 0.5, 0.95, 0.99)http_requests_errors- Total number of HTTP errors (4xx, 5xx)
-
Business Metrics:
objectstore_objects_created_total- Total objects createdobjectstore_objects_updated_total- Total objects updatedobjectstore_objects_deleted_total- Total objects deletedobjectstore_objects_retrieved_total- Total objects retrieved
-
Endpoint Metrics:
objectstore_api_seconds- API endpoint latency (tagged by endpoint)objectstore_api_create_or_update_seconds- Create/update endpoint latencyobjectstore_api_get_by_key_seconds- Get by key endpoint latencyobjectstore_api_get_all_seconds- Get all endpoint latencyobjectstore_api_delete_seconds- Delete endpoint latency
Prometheus Configuration:
The project includes a prometheus.yml configuration file that's ready to use. When running with Docker Compose, Prometheus is automatically configured to scrape metrics from the application.
For Docker Compose setup, see DOCKER.md. Prometheus is included in the docker-compose.yml file and will start automatically when you run docker-compose up -d.
Prometheus UI will be available at: http://localhost:9090
If you want to run Prometheus separately (without Docker Compose):
-
Download Prometheus:
# macOS (using Homebrew) brew install prometheus # Or download from https://prometheus.io/download/
-
Run Prometheus:
# Navigate to project directory cd /path/to/MercorProj # Run Prometheus with the included config prometheus --config.file=prometheus.yml --storage.tsdb.path=./prometheus-data
-
Access Prometheus UI:
- Open http://localhost:9090 in your browser
For Docker setup, see DOCKER.md.
Once Prometheus is running, you can query metrics using the Prometheus Query Language (PromQL):
-
Open Prometheus UI: http://localhost:9090
-
Example Queries:
Request Rate (requests per second):
rate(http_requests_total[5m])Request Latency (95th percentile):
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))Total Objects Created:
objectstore_objects_created_totalError Rate:
rate(http_requests_errors[5m])API Endpoint Latency:
rate(objectstore_api_seconds_sum[5m]) / rate(objectstore_api_seconds_count[5m])Request Count by Method:
sum by (method) (rate(http_requests_total[5m]))Request Count by Status:
sum by (status) (http_requests_total) -
Viewing Targets:
- Navigate to Status → Targets to verify the application is being scraped
- The
objectstoretarget should show as UP
-
Graphing Metrics:
- Enter a query in the "Expression" field
- Click Execute
- Switch to Graph tab to see the visualization
- Use the time range selector to adjust the time window
The project includes Prometheus Alertmanager for email notifications. Alerts are configured in alert_rules.yml and sent via email.
-
Configure Gmail App Password (see ALERT_SETUP.md for detailed instructions):
- Enable 2-Step Verification on your Gmail account
- Generate an App Password at https://myaccount.google.com/apppasswords
- Update
alertmanager.ymlwith your App Password
-
Start Services:
- For Docker Compose setup, see DOCKER.md
- Or run Prometheus and Alertmanager manually
-
Access Alertmanager UI: http://localhost:9093
A test alert is configured that fires immediately to verify email notifications:
- Alert Name: TestAlert
- Purpose: Verify alerting is working
- Email Recipient: samgearou@gmail.com
- Fires: Immediately when Prometheus starts
- TestAlert: Fires immediately (for testing email setup)
- HighErrorRate: Error rate > 5% for 2 minutes
- CriticalErrorRate: Error rate > 20% for 1 minute
- HighLatency: 95th percentile latency > 1s for 5 minutes
- CriticalLatency: 95th percentile latency > 5s for 2 minutes
- ServiceDown: Service unavailable for 1 minute
- LowRequestRate: Request rate < 0.01 req/s for 10 minutes
- HighMemoryUsage: JVM heap > 90% for 5 minutes
- Prometheus Alerts: http://localhost:9090/alerts
- Alertmanager UI: http://localhost:9093
- Alert Status: Check Alertmanager for active/firing alerts
For more details, see ALERT_SETUP.md
The prometheus.yml file includes:
- Scrape interval: 15 seconds (10 seconds for application metrics)
- Retention: 30 days (when using Docker Compose)
- Targets:
- Prometheus self-monitoring (localhost:9090)
- Object Store Application (app:8080)
You can customize the configuration by editing prometheus.yml.
GET /actuator/metrics
Lists all available metric names.
Example using curl:
# List all metrics
curl http://localhost:8080/actuator/metrics
# Get specific metric
curl http://localhost:8080/actuator/metrics/http.requests.totalHere are practical curl examples you can use to interact with the API. Make sure the application is running on http://localhost:8080 before executing these commands.
# Comprehensive health check (includes database status)
curl http://localhost:8080/healthhttp://localhost:8080/health
# Simple health check
curl http://localhost:8080/health/simple
# Spring Boot Actuator health check
curl http://localhost:8080/actuator/healthExpected Response:
{
"status": "UP",
"service": "Object Store API",
"database": "UP"
}curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "my-first-key", "content": "This is my first text object"}'Expected Response:
{
"id": 1,
"key": "my-first-key",
"content": "This is my first text object",
"createdAt": "2026-01-02T15:41:43.123456",
"updatedAt": null
}curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "my-second-key", "content": "This is my second text object with different content"}'curl http://localhost:8080/api/objects/my-first-keyExpected Response:
{
"id": 1,
"key": "my-first-key",
"content": "This is my first text object",
"createdAt": "2026-01-02T15:41:43.123456",
"updatedAt": null
}curl http://localhost:8080/api/objectsExpected Response:
[
{
"id": 1,
"key": "my-first-key",
"content": "This is my first text object",
"createdAt": "2026-01-02T15:41:43.123456",
"updatedAt": null
},
{
"id": 2,
"key": "my-second-key",
"content": "This is my second text object with different content",
"createdAt": "2026-01-02T15:42:00.123456",
"updatedAt": null
}
]curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "my-first-key", "content": "This is the updated content for my first key"}'Expected Response:
{
"id": 1,
"key": "my-first-key",
"content": "This is the updated content for my first key",
"createdAt": "2026-01-02T15:41:43.123456",
"updatedAt": "2026-01-02T15:45:00.123456"
}curl -X DELETE http://localhost:8080/api/objects/my-second-keyExpected Response: HTTP 204 No Content (empty response body)
curl -v http://localhost:8080/api/objects/non-existent-keyExpected Response: HTTP 404 Not Found
curl -v -X DELETE http://localhost:8080/api/objects/non-existent-keyExpected Response: HTTP 404 Not Found
curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "special-chars-key", "content": "Content with special chars: !@#$%^&*() and newlines\nand tabs\t"}'curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "json-content-key", "content": "{\"name\": \"John\", \"age\": 30}"}'To get formatted JSON output, pipe the response through jq (if installed):
curl http://localhost:8080/api/objects | jqOr use Python for formatting:
curl http://localhost:8080/api/objects | python -m json.toolcurl http://localhost:8080/api/objects -o response.jsoncurl -i http://localhost:8080/api/objectscurl -v -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "debug-key", "content": "Debug content"}'curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"content": "Missing key field"}'Expected Response: HTTP 400 Bad Request
curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "", "content": "Empty key"}'Expected Response: HTTP 400 Bad Request
Run this sequence to test all operations:
# 1. Create an object
curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "test-key", "content": "Initial content"}'
# 2. Retrieve it
curl http://localhost:8080/api/objects/test-key
# 3. Update it
curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "test-key", "content": "Updated content"}'
# 4. Get all objects
curl http://localhost:8080/api/objects
# 5. Delete it
curl -X DELETE http://localhost:8080/api/objects/test-key
# 6. Verify it's deleted (should return 404)
curl -v http://localhost:8080/api/objects/test-keyPOST /api/objects
Creates a new text object or updates an existing one if the key already exists.
Request Body:
{
"key": "my-unique-key",
"content": "This is the text content to store"
}Response (200 OK):
{
"id": 1,
"key": "my-unique-key",
"content": "This is the text content to store",
"createdAt": "2026-01-02T15:41:43",
"updatedAt": null
}Example using curl:
curl -X POST http://localhost:8080/api/objects \
-H "Content-Type: application/json" \
-d '{"key": "example-key", "content": "Hello, World!"}'curl -X GET http://localhost:8080/api/objects/example-key \ 03:49:53 PM
-H "Content-Type: application/json"Retrieves a text object by its unique key.
Response (200 OK):
{
"id": 1,
"key": "example-key",
"content": "Hello, World!",
"createdAt": "2026-01-02T15:41:43",
"updatedAt": null
}Response (404 Not Found) if key doesn't exist:
(empty response)
Example using curl:
curl http://localhost:8080/api/objects/example-keycurl -X GET http://localhost:8080/api/objects \ 03:49:53 PM
-H "Content-Type: application/json"Retrieves all text objects stored in the database.
Response (200 OK):
[
{
"id": 1,
"key": "example-key",
"content": "Hello, World!",
"createdAt": "2026-01-02T15:41:43",
"updatedAt": null
},
{
"id": 2,
"key": "another-key",
"content": "Another text object",
"createdAt": "2026-01-02T15:42:00",
"updatedAt": "2026-01-02T15:43:00"
}
]Example using curl:
curl http://localhost:8080/api/objectsDELETE /api/objects/{key}
Deletes a text object by its unique key.
Response (204 No Content) if successful
Response (404 Not Found) if key doesn't exist
Example using curl:
curl -X DELETE http://localhost:8080/api/objects/example-keyConnect to the database:
# macOS (Homebrew)
PGPASSWORD=postgres /opt/homebrew/opt/postgresql@14/bin/psql -U postgres -d objectstore
# Linux
sudo -u postgres psql -d objectstoreThen run SQL queries:
-- View all objects
SELECT * FROM text_objects;
-- View specific object
SELECT * FROM text_objects WHERE key = 'example-key';
-- Count objects
SELECT COUNT(*) FROM text_objects;# Start PostgreSQL
brew services start postgresql@14
# Stop PostgreSQL
brew services stop postgresql@14
# Restart PostgreSQL
brew services restart postgresql@14
# Check status
brew services list | grep postgresql@14# Start PostgreSQL
sudo systemctl start postgresql
# Stop PostgreSQL
sudo systemctl stop postgresql
# Restart PostgreSQL
sudo systemctl restart postgresql
# Check status
sudo systemctl status postgresql
# Enable auto-start on boot
sudo systemctl enable postgresql-
Check if PostgreSQL is running:
# macOS brew services list | grep postgresql # Linux sudo systemctl status postgresql
-
Verify database connection:
# Test connection PGPASSWORD=postgres psql -U postgres -d objectstore -c "SELECT 1;"
-
Check application logs for specific error messages
- Ensure PostgreSQL is running on port 5432
- Verify database credentials in
application.properties - Check if PostgreSQL is listening:
lsof -i :5432(macOS/Linux)
Change the port in application.properties:
server.port=8081Create it manually:
CREATE DATABASE objectstore;MercorProj/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/mercor/objectstore/
│ │ │ ├── ObjectStoreApplication.java # Main application class
│ │ │ ├── controller/
│ │ │ │ └── TextObjectController.java # REST API endpoints
│ │ │ ├── model/
│ │ │ │ └── TextObject.java # Entity model
│ │ │ ├── repository/
│ │ │ │ └── TextObjectRepository.java # Data access layer
│ │ │ └── service/
│ │ │ └── TextObjectService.java # Business logic
│ │ └── resources/
│ │ └── application.properties # Configuration
│ └── test/
│ └── java/
│ └── com/mercor/objectstore/
│ ├── controller/
│ │ └── TextObjectControllerTest.java # Controller unit tests
│ └── service/
│ └── TextObjectServiceTest.java # Service unit tests
├── target/ # Build output
└── pom.xml # Maven configuration
- Java 11
- Spring Boot 2.7.14
- Spring Data JPA
- Hibernate
- PostgreSQL 14+
- Maven
This project is provided as-is for demonstration purposes.