A production-grade RESTful API built with Spring Boot demonstrating backend engineering through JSON document management. Implements ETag-based optimistic concurrency control, RFC 7386-compliant JSON Merge Patch for partial updates, and Google OAuth 2.0 with RS256 JWT validation for security. Features a distributed architecture with Redis for high-performance transactional storage, Elasticsearch with parent-child join relations for hierarchical document indexing and full-text search, and RabbitMQ for asynchronous message-driven indexing between data stores.
This application demonstrates a distributed, event-driven architecture with multiple data stores:
- Redis: Primary transactional data store for high-performance key-value operations
- Elasticsearch: Secondary store with parent-child join relations for hierarchical document indexing and full-text search capabilities
- RabbitMQ: Message broker enabling asynchronous, decoupled synchronization between Redis and Elasticsearch
- Event-Driven Design: All write operations (CREATE, UPDATE, DELETE) publish messages to RabbitMQ, which are consumed by listeners that update Elasticsearch indices asynchronously
Client Request → REST Controller → Redis (Primary Store)
↓
RabbitMQ Message Queue
↓
Message Listener → Elasticsearch (Search Index)
- **Create (POST /api/plans):**
Creates a new plan resource.
- If a plan with the same `objectId` does not exist, it creates the resource (returns **201 Created**).
- If a plan with the same `objectId` exists and the incoming JSON is identical (ETag match), returns **304 Not Modified**.
- If a plan with the same `objectId` exists but with different content, it overwrites the resource and returns **201 Created**.
- **Read (GET /api/plans/{objectId}):**
Retrieves a plan by its `objectId` with conditional read support using ETag/If-None-Match.
- **Read All (GET /api/plans):**
- Returns a list of all stored plans.
- **Update (PATCH /api/plans/{objectId}):**
Supports JSON Merge Patch for partial updates with conditional write using the `If-Match` header.
- If the request header doesn't contain the `If-Match` header, or if the ETag value is not the latest one, returns **412 Precondition Failed**.
- If the PATCH payload makes no effective change (ETag remains the same), returns **304 Not Modified**.
- **Delete (DELETE /api/plans/{objectId}):**
- Deletes a plan resource by its `objectId` and publishes a delete event to RabbitMQ for Elasticsearch cleanup.
- All incoming JSON payloads are validated against a pre-defined JSON Schema (
plan-schema.json) using the NetworkNT JSON Schema Validator. - Ensures data integrity and contract compliance before persistence.
- ETags are computed dynamically using MD5 hashing of JSON content to support conditional reads and writes.
- Conditional Reads: GET requests use
If-None-Matchheader for efficient caching (returning 304 Not Modified if ETag matches). - Conditional Writes: PATCH requests require an
If-Matchheader to prevent lost updates (returning 412 Precondition Failed if ETag doesn't match). - Prevents race conditions and ensures data consistency in distributed systems.
- Implements JSON Merge Patch (RFC 7386) for partial updates with intelligent array merging.
- Supports nested object merging and array element matching by
objectId. - If a patch element with a new
objectIdis provided, it's automatically appended to the array.
- Redis: Serves as the source of truth for transactional consistency and low-latency reads/writes.
- Elasticsearch: Provides advanced search capabilities with parent-child join relations for hierarchical document structure:
plan→linkedPlanService,planCostShare(level 1)linkedPlanService→planserviceCostShare,linkedService(level 2)
- RabbitMQ: Decouples write operations from indexing, enabling asynchronous Elasticsearch updates.
- All write operations (CREATE, PATCH, DELETE) publish messages to RabbitMQ topic exchange.
PlanIndexListenerconsumes messages and updates Elasticsearch indices asynchronously.- Routing keys enable flexible message handling (
plan.create,plan.patch,plan.delete).
- All endpoints are secured using Bearer tokens issued by Google Identity Platform.
- Spring Security's OAuth2 Resource Server validates JWT tokens signed with RS256.
- JWK Set URI auto-rotation ensures cryptographic key freshness.
- Description: Create a new plan and store it in Redis.
- Request Body Example:
{ "planCostShares": { "deductible": 2000, "_org": "example.com", "copay": 23, "objectId": "1234vxc2324sdf-501", "objectType": "membercostshare" }, "linkedPlanServices": [ { "linkedService": { "_org": "example.com", "objectId": "1234520xvc30asdf-502", "objectType": "service", "name": "Yearly physical" }, "planserviceCostShares": { "deductible": 10, "_org": "example.com", "copay": 0, "objectId": "1234512xvc1314asdfs-503", "objectType": "membercostshare" }, "_org": "example.com", "objectId": "27283xvx9asdff-504", "objectType": "planservice" } ], "_org": "example.com", "objectId": "12xvxc345ssdsds-508", "objectType": "plan", "planType": "inNetwork", "creationDate": "12-12-2017" } - Response:
- 201 Created: Resource created or overwritten.
- 304 Not Modified: If the resource already exists with identical content.
Headers:Location:/api/plans/{objectId}ETag:<computed-etag>
- Description: Retrieve the plan data by
objectId. Supports conditional reads usingETag. - Headers:
If-None-Match:<etag>(optional)
- Response:
200 OKwith JSON body if the resource exists and ETag doesn't match/not provided.304 Not Modifiedif theIf-None-Matchheader matches the current ETag.404 Not Foundif the resource does not exist.
-
Description: Retrieves all stored plans.
-
Response:
200 OKwith a JSON array of plans.404 Not Foundif resource does not exist.
-
Description: Applies a JSON Merge Patch (RFC 7386) to update a plan.
-
Conditional Write:
- Requires an
If-Matchheader that must match the computed ETag. If it doesn't match, returns 412 Precondition Failed - If the patch makes no effective change, returns 304 Not Modified.
- If element with new
objectIdis provided in the patch payload, a new object is created and appended.
- Requires an
-
Response:
200 OK: Returns the updated JSON and new ETag.304 Not Modified: If no effective change is made.412 Precondition Failed:If theIf-Matchheader does not match.
- Description: Delete the plan by
objectId. - Response:
204 No Contentif the object was deleted successfully.404 Not Foundif the object does not exist.
- Java 17+
- Maven 3.6+
- Redis 6+ (either installed locally or running in Docker)
- Elasticsearch 8.x (running locally or in Docker)
- RabbitMQ 3.x (running locally or in Docker)
- Google Cloud Account for OAuth 2.0 credentials
-
Start Infrastructure Services:
Using Docker Compose (recommended):
docker-compose up -d
Or manually:
Redis:
docker run --name redis -p 6379:6379 -d redis
Elasticsearch:
docker run --name elasticsearch -p 9200:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" -d elasticsearch:8.17.4
RabbitMQ:
docker run --name rabbitmq -p 5672:5672 -p 15672:15672 -d rabbitmq:3-management
-
Clone the repository:
git clone <repository-url> cd json-query-engine
-
Build and run the application:
./mvnw spring-boot:run # The API will start on port 8081 -
Configure Google OAuth 2.0:
- Create OAuth 2.0 credentials in Google Cloud Console.
- Configure your OAuth Client (for Postman testing, use
https://oauth.pstmn.io/v1/callbackas the redirect URI). - Use the Client ID and Client Secret to obtain a Bearer token.
- The API's security configuration validates Google's JWT tokens automatically.
Edit the following properties in src/main/resources/application.properties:
# Server
server.port=8081
# Redis (Primary Store)
spring.data.redis.host=localhost
spring.data.redis.port=6379
# Elasticsearch (Search Index)
spring.elasticsearch.uris=http://localhost:9200
# RabbitMQ (Message Broker)
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
# OAuth2 Resource Server (Google IDP)
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://accounts.google.com
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certsWhen testing with Postman, you need to configure OAuth 2.0 to obtain a valid Google-signed JWT token, then set your requests to inherit that authentication.
This screenshot shows how to set up OAuth 2.0 at the Collection level, so individual requests can inherit it.

After setting up OAuth 2.0, click "Get New Access Token" to start the Google login flow.

Here, Postman prompts you to select the Google account for which you want to grant access.

Google shows a consent screen asking you to allow Postman to access your profile and email.

Finally, each request can inherit the OAuth 2.0 configuration from the parent collection, making it easier to manage tokens.

-
Port already in use:
Update the port inapplication.propertiesor free the port using:# Windows netstat -ano | findstr :8081 taskkill /PID <PID> /F # Linux/Mac lsof -ti:8081 | xargs kill -9
-
Redis connection errors:
Ensure Redis is running and accessible atlocalhost:6379:redis-cli ping # Should return "PONG" -
Elasticsearch connection errors:
Verify Elasticsearch is running:curl http://localhost:9200 # Should return cluster info -
RabbitMQ connection errors:
Check RabbitMQ status and management console athttp://localhost:15672(default credentials: guest/guest). -
OAuth 2.0 Errors:
- Verify that the Bearer token is valid and not expired.
- Ensure the token is issued by Google (
iss: https://accounts.google.com). - Check that the security configuration matches the issuer URI.
- Spring Boot 3.4.2 (Java 17)
- Spring Web (REST API)
- Spring Security (OAuth2 Resource Server)
- Redis (Primary transactional key-value store)
- Elasticsearch 8.17.4 (Search index with parent-child join relations)
- RabbitMQ (AMQP message broker)
- Spring AMQP (Message publishing and consumption)
- Google OAuth 2.0 (Identity Provider)
- JWT with RS256 (Token validation)
- JSON Schema Validator (NetworkNT) (Payload validation)
- Jackson (JSON serialization/deserialization)
- JSON Merge Patch (RFC 7386) (Partial updates)
- Maven (Dependency management and build tool)
- Lombok (Boilerplate reduction)
- Docker (Containerization)
This project demonstrates several production-ready backend engineering patterns:
-
Polyglot Persistence: Strategic use of multiple specialized data stores (Redis for speed, Elasticsearch for search) rather than forcing a single database to handle all use cases.
-
Event-Driven Architecture: Decoupled write and indexing operations using message queues, enabling horizontal scalability and fault tolerance.
-
Optimistic Concurrency Control: ETag-based conditional requests prevent lost updates and race conditions without pessimistic locking overhead.
-
REST API Best Practices:
- Proper HTTP semantics (201 Created, 304 Not Modified, 412 Precondition Failed)
- RFC-compliant implementations (RFC 7386 for JSON Merge Patch)
- Conditional requests (If-Match, If-None-Match headers)
-
Parent-Child Document Relationships: Elasticsearch join relations enable efficient querying of hierarchical data while maintaining denormalization benefits.
-
Asynchronous Processing: Non-blocking indexing operations improve API response times and user experience.
-
Security: Industry-standard OAuth 2.0 with JWT validation, ensuring stateless authentication scalability.
-
Contract-First Design: JSON Schema validation ensures API contracts are enforced at runtime.
Contributions are welcome! Please fork the repository and submit a pull request with your changes.
This project is licensed under the MIT License.
For questions or issues, please reach out to:
Eashan – eashanroy7@gmail.com