This project is a Spring Boot application built with Maven. To get started, ensure you have the following prerequisites installed:
- Java Development Kit (JDK) 21 or higher
- Maven 3.9 or higher
- Docker or Podman
The application uses PostgreSQL + PostGIS as its database. You can run an instance using Podman (or Docker) with the following command:
mkdir -p ./postgis_data && podman run -d --name postgis -e POSTGRES_PASSWORD=X.yz123 \
-v ./src/main/resources/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:Z \
-v ./postgis_data:/var/lib/postgresql/data:Z \
-p 5432:5432 postgis/postgisThis will start a PostgreSQL container with PostGIS extension enabled. The database will be initialized with the schema defined in init-db.sql.
You can run the Spring Boot application using Maven (in the project directory):
mvn spring-boot:runThis will start the application which will connect to the previously started PostGIS DB. The app will have an embedded ActiveMQ Artemis broker for JMS messaging.
(note that the first execution will take a couple of minutes to populate the dummy data in the DB)
PostgreSQL with the PostGIS extension was chosen for this demo app due to its robust support for geographic data types and spatial queries while also keeping the standard ACID properties of a relational database. ACID compliance is crucial for order processing systems to ensure data integrity and consistency, like not overselling stock for example.
The design of this demo app is that the /orders endpoint accepts orders and processes them asynchronously using JMS listeners. This guarantees that the endpoint is responsive and can handle high throughput. The orders endpoint will only validate the input and attempt to convert the shipping address to a geographic location.
The GeocodingService is a stub that simulates geocoding by generating coordinates within the continental US based on a hash of the address string. This guarantees the same address will always yield the same coordinates. Empty addresses are considered invalid. You can also include "Fail St" in the address to simulate geocoding failures.
If the order passes basic validation, it is persisted in the database with a PENDING status. A JMS message is sent to trigger the warehouse matching process.
The warehouse matching process uses an optimistic algorithm to find the nearest warehouse with sufficient stock. Warehouses are sorted by proximity to the shipping location, and the first warehouse with enough stock is selected. The sorting is very efficient because of the spatial indexing provided by PostGIS and for each warehouse we collect the relevant stock rows in a single query using an IN clause. This reduces the number of queries needed to find a match.
If a matching warehouse is found, the order status is updated to RESERVED. In the same transaction, the available stock is decremented from the warehouse and moved to reserved stock. This ensures that the stock is not oversold. Finally, a JMS message is sent to trigger the payment process.
If no matching warehouse is found, the order status is updated to NOT_PROCURABLE, indicating that the order cannot be fulfilled.
The payment listener is very simple. It simply builds a PaymentRequest from the order details and simulates a payment processing. The mocked payment processing will accept any payment as long as the credit card CVV is not "666". You can use this to simulate payment failures.
If the payment is accepted, the order status is updated to PAYMENT_ACCEPTED. Otherwise, it is updated to PAYMENT_REJECTED. The scope of this demo app ends here, but in a real application, further steps would be taken to prepare the order for shipment. If the order ends up in PAYMENT_REJECTED or remains in RESERVED for too long without payment confirmation, a scheduled cleanup task could vacate the order. Vacating an order means that any reserved stock is released back to available stock, and the order is marked as vacated.
This demo app is provided as a monolithic application for simplicity, but in a real-world scenario, the different components (like order processing, warehouse management, payment processing) would likely be implemented as separate microservices.
graph TD
%% Primary Flow
START((START)) --> PENDING
PENDING -- "Warehouse Matching (JMS)" --> MATCHING_LOGIC{Match Result}
MATCHING_LOGIC -- Success --> RESERVED
MATCHING_LOGIC -- Failure --> NOT_PROCURABLE
RESERVED -- "Payment Call (JMS)" --> PAYMENT_LOGIC{Payment Result}
PAYMENT_LOGIC -- Success --> PAYMENT_ACCEPTED
PAYMENT_LOGIC -- Failure --> PAYMENT_REJECTED
%% Expiration / Cleanup Flow (Scheduler)
PAYMENT_REJECTED -- "Cleanup Task (Grace Period)" --> VACATED
RESERVED -- "Cleanup Task (Timeout)" --> VACATED
%% Future Suggested States
PAYMENT_ACCEPTED -- "Warehouse Pickup" --> IN_PREPARATION
IN_PREPARATION -- "Courier Scanned" --> SHIPPED
SHIPPED -- "Final Delivery" --> DELIVERED
%% Final States
NOT_PROCURABLE --> END((END))
VACATED --> END
DELIVERED --> END
style PENDING fill:#404040,stroke:#222
style MATCHING_LOGIC fill:#404040,stroke:#222
style VACATED fill:#404040,stroke:#222
style PAYMENT_ACCEPTED fill:#609060,stroke:#333
style IN_PREPARATION fill:#609060,stroke:#333
style SHIPPED fill:#609060,stroke:#333
style DELIVERED fill:#409040,stroke:#333
style NOT_PROCURABLE fill:#A05454,stroke:#333
style PAYMENT_REJECTED fill:#A05454,stroke:#333
style RESERVED fill:#606D60,stroke:#333
style START fill:#777777,stroke:#333,stroke-width:2px
style END fill:#777777,stroke:#333,stroke-width:2px
# Inexisting item (not even persisted)
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"shippingAddress": "700 Pennsylvania Avenue NW, Washington, DC",
"creditCardNumber": "4111222233334444",
"creditCardExpiryDate": "1226",
"creditCardCvv": "123",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 1212601, "quantity": 2 },
{ "id": 1051, "quantity": 1 }
]
}'
# Inexisting customer (not even persisted)
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1212121,
"shippingAddress": "700 Pennsylvania Avenue NW, Washington, DC",
"creditCardNumber": "4111222233334444",
"creditCardExpiryDate": "1226",
"creditCardCvv": "123",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 601, "quantity": 2 },
{ "id": 1051, "quantity": 1 }
]
}'
# Incorrect credit card format (not even persisted)
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"shippingAddress": "700 Pennsylvania Avenue NW, Washington, DC",
"creditCardNumber": "41112F22233334444",
"creditCardExpiryDate": "1226",
"creditCardCvv": "123",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 601, "quantity": 2 },
{ "id": 1051, "quantity": 1 }
]
}'
# Incorrect credit card CVV (not even persisted)
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"shippingAddress": "700 Pennsylvania Avenue NW, Washington, DC",
"creditCardNumber": "4111222233334444",
"creditCardExpiryDate": "1226",
"creditCardCvv": "12345",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 601, "quantity": 2 },
{ "id": 1051, "quantity": 1 }
]
}'
# Incorrect credit card expiry (not even persisted)
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"shippingAddress": "700 Pennsylvania Avenue NW, Washington, DC",
"creditCardNumber": "4111222233334444",
"creditCardExpiryDate": "4526",
"creditCardCvv": "123",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 601, "quantity": 2 },
{ "id": 1051, "quantity": 1 }
]
}'
# Bad address (not even persisted) (this triggers the mock fail via Fail St)
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"shippingAddress": "123 Fail St, New York, NY",
"creditCardNumber": "4111222233334444",
"creditCardExpiryDate": "0526",
"creditCardCvv": "123",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 601, "quantity": 2 },
{ "id": 1051, "quantity": 1 }
]
}'
# Correct order
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"shippingAddress": "700 Pennsylvania Avenue NW, Washington, DC",
"creditCardNumber": "4111222233334444",
"creditCardExpiryDate": "1226",
"creditCardCvv": "123",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 601, "quantity": 2 },
{ "id": 1051, "quantity": 1 }
]
}'
# Rejected payment (via CVV:666)
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"shippingAddress": "700 Pennsylvania Avenue NW, Washington, DC",
"creditCardNumber": "4111222233334444",
"creditCardExpiryDate": "1226",
"creditCardCvv": "666",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 601, "quantity": 2 },
{ "id": 1051, "quantity": 1 }
]
}'
# Not procurable
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"shippingAddress": "700 Pennsylvania Avenue NW, Washington, DC",
"creditCardNumber": "4111222233334444",
"creditCardExpiryDate": "1226",
"creditCardCvv": "123",
"creditCardOrderMemo": "My stuff",
"items": [
{ "id": 601, "quantity": 20000000 },
{ "id": 1051, "quantity": 1 }
]
}'