This repository contains a small Symfony 6.4 microservices monorepo for the technical task.
The system consists of two independent services:
product-serviceis the source of truth for products and publishes product synchronization eventsorder-servicemaintains a local copy of products and handles order creation
Services communicate asynchronously via RabbitMQ.
Flow:
- A product is created in
product-service - A
ProductSyncedMessageis published to RabbitMQ order-serviceconsumes the message and updates its local product table- An order is created in
order-serviceusing the local product read model order-servicepublishes anOrderCreatedMessageproduct-serviceconsumes the message, validates quantity, decreases it, and republishesProductSyncedMessageorder-serviceconsumes the updatedProductSyncedMessageand refreshes its local product copy
This design demonstrates:
- separation of concerns
- asynchronous communication
- eventual consistency
product-service: Symfony application for product managementorder-service: Symfony application for order managementshared-bundle: reusable shared package for common codedocker-compose.yml: local infrastructure and app runtime
- Symfony
6.4 - PHP
8.4 - PostgreSQL
16 - RabbitMQ with management UI
- Docker Compose for local development
product-service:http://localhost:8001order-service:http://localhost:8002- RabbitMQ AMQP:
amqp://localhost:5672 - RabbitMQ management UI:
http://localhost:15672 product-db:localhost:5433order-db:localhost:5434adminer(optional):http://localhost:8080
Start the stack:
docker compose up --build
Run in background:
docker compose up --build -d
On a clean first run, product-service and order-service automatically execute Doctrine migrations during container startup. The first boot may take a little longer while the databases are initialized.
Start Adminer as well:
docker compose --profile tools up --build
Stop the stack:
docker compose down
After a clean reset such as:
docker compose down -v
starting the stack is enough:
docker compose up --build -d
The API containers run their Doctrine migrations automatically on startup, and the order-consumer container starts afterward to process RabbitMQ messages in the background.
- Start the stack:
docker compose up --build -d
-
The
product-serviceandorder-serviceconsumers are started automatically via Docker Compose. -
Create a product in
product-service:
curl -X POST http://localhost:8001/products \
-H "Content-Type: application/json" \
-d '{"name":"Coffee Mug","price":12.99,"quantity":100}'
- Verify that the product was synchronized to
order-service.
You can:
- check the database (order-service product table), or
- proceed with order creation below, which implicitly validates synchronization
- Create an order in
order-service:
curl -X POST http://localhost:8002/orders \
-H "Content-Type: application/json" \
-d '{"productId":"<PRODUCT_ID>","customerName":"John Doe","quantityOrdered":2}'
Expected result:
- Order is successfully created
- Product quantity is decreased in
product-service order-servicereceives the updated quantity asynchronously throughProductSyncedMessage
- Check created orders:
curl http://localhost:8002/orders
Test structure:
product-service/tests: unit and integration tests for product logicorder-service/tests: unit and integration tests for order logic and product synchronizationtests/e2e: end-to-end tests for cross-service behavior
Test coverage includes:
- Product creation and validation
- Order creation with business rules
- Product synchronization logic
- End-to-end scenarios covering full flow and failure cases
Run product-service tests:
make test-product
or:
cd product-service && ./vendor/bin/simple-phpunit
Run order-service tests:
make test-order
or:
cd order-service && ./vendor/bin/simple-phpunit
Run end-to-end tests:
make test-e2e
or:
php tests/e2e/run.php
The end-to-end runner starts an isolated Docker Compose project with separate volumes and alternate ports, so it does not modify the main local databases or RabbitMQ state.
The end-to-end tests validate the full product-to-order flow, including:
- successful order creation
- insufficient quantity failure
- missing product failure
The product-service API uses the same four product fields everywhere:
idas UUIDnamepricequantity
Example:
{
"id": "018f4b0c-8ee8-7d15-bc28-0c65c8e0e9aa",
"name": "Coffee Mug",
"price": 12.99,
"quantity": 100
}
Each service can run directly on the host machine.
Example:
cd product-service
composer install
symfony server:start
or:
cd product-service
composer install
php -S 127.0.0.1:8000 -t public
Do the same for order-service.
When running outside Docker, defaults expect:
- PostgreSQL for product-service on
localhost:5433 - PostgreSQL for order-service on
localhost:5434 - RabbitMQ on
localhost:5672
Authentication and authorization are intentionally not implemented because they are outside the scope of this task.
In a production system, access control would typically be implemented using:
- JWT-based authentication
- OAuth2 / OpenID Connect
- API Gateway or identity provider
- Product updates and deletions are not fully implemented
- Order lifecycle is simplified (single "Processing" state)
OrderCompletedMessageandOrderRejectedMessageare not implemented- No failure handling, compensation logic, or retry orchestration is implemented between services
- No retry or dead-letter queue handling for failed messages
- Eventual consistency is used instead of distributed transactions