A financial transaction system with an asynchronous command queue for processing operations.
# Clone and start services (PostgreSQL + 3 workers)
git clone https://github.com/HomelessCoder/financial-queue-system.git
cd financial-queue-system
docker compose up -d
Terminal 1 - Watch workers process commands in real-time:
docker compose logs -f
Terminal 2 - Execute the following commands:
Step 1: Deposit funds to John Doe's account
# Deposit $500 to John Doe
docker compose exec worker-1 php bin/console trade:deposit-funds 0199dfbd-9da7-7e4c-8754-13fa5a9af89c 50000 USD
# Check account balance
docker compose exec postgres psql -U fqs -d fqs -c "SELECT * FROM accounts WHERE id = '0199dfbd-9da7-7e4c-8754-13fa5a9af89c';"
Step 2: Charge funds from the account
# Charge $200 from John Doe
docker compose exec worker-1 php bin/console trade:charge-funds 0199dfbd-9da7-7e4c-8754-13fa5a9af89c 20000 USD "Service fee"
# Check account and transactions
docker compose exec postgres psql -U fqs -d fqs -c "SELECT * FROM accounts WHERE id = '0199dfbd-9da7-7e4c-8754-13fa5a9af89c';"
docker compose exec postgres psql -U fqs -d fqs -c "SELECT * FROM transactions WHERE target_account_id = '0199dfbd-9da7-7e4c-8754-13fa5a9af89c' ORDER BY timestamp DESC LIMIT 5;"
Step 3: Create a hold
# Create a $300 hold on John Doe's account (expires in 30 minutes)
docker compose exec worker-1 php bin/console trade:place-hold 0199dfbd-9da7-7e4c-8754-13fa5a9af89c 30000 USD "Pending payment" --expires-in-minutes=30
# Check account, transactions, and holds
docker compose exec postgres psql -U fqs -d fqs -c "SELECT * FROM accounts WHERE id = '0199dfbd-9da7-7e4c-8754-13fa5a9af89c';"
docker compose exec postgres psql -U fqs -d fqs -c "SELECT * FROM transactions WHERE target_account_id = '0199dfbd-9da7-7e4c-8754-13fa5a9af89c' ORDER BY timestamp DESC LIMIT 5;"
docker compose exec postgres psql -U fqs -d fqs -c "SELECT * FROM holds WHERE account_id = '0199dfbd-9da7-7e4c-8754-13fa5a9af89c';"
# Capture the hold (use the hold ID from the previous output)
# Example hold ID: 0199e556-cdf6-7262-b67c-48ce3f3a2ff4
docker compose exec worker-1 php bin/console trade:capture-hold <hold-id>
Step 4: Transfer funds to Jane Smith
# Transfer $100 from John Doe to Jane Smith
docker compose exec worker-1 php bin/console trade:transfer-funds 0199dfbd-9da7-7e4c-8754-13fa5a9af89c 0199dfbd-9da7-78b9-b132-8843318e7348 10000 USD
# Check both accounts
docker compose exec postgres psql -U fqs -d fqs -c "SELECT id, balance_units, balance_currency_iso, held_balance_units FROM accounts WHERE id IN ('0199dfbd-9da7-7e4c-8754-13fa5a9af89c', '0199dfbd-9da7-78b9-b132-8843318e7348');"
π‘ Tip: Watch Terminal 1 to see workers pick up and process commands in real-time!
Workers safely run in multiple processes simultaneously:
# Docker Compose runs 3 workers in parallel
docker compose up -d
# Workers use PostgreSQL FOR UPDATE SKIP LOCKED to prevent race conditions
# Each worker processes different commands concurrently
# Implementation: src/CommandQueue/Infra/CommandQueueRepository.php
Workers can run for any duration and in any quantity:
# Add more workers in docker-compose.yml
# Workers automatically pick up commands using SKIP LOCKED
# No coordination required between worker instances
All operations are protected against double processing and double writes:
- Database Row Locking:
FOR UPDATE SKIP LOCKED
prevents concurrent command processing - Idempotency Keys: Optional UUID keys prevent duplicate operations (charge, deposit, transfer, hold)
- Transaction Integrity: All operations wrapped in database transactions with automatic rollback on failure
- Status Tracking: Command lifecycle (pending β processing β completed/failed)
Example with idempotency key:
# If retried, same idempotency key prevents duplicate charge
docker compose exec worker-1 php bin/console trade:charge-funds <account-id> 5000 USD "Purchase" --idempotency-key=<uuid>
# Run test suite
vendor/bin/phpunit test/
# Code quality checks
make phpstan
make codestyle
Component | Technology |
---|---|
Language | PHP 8.4+ |
Database | PostgreSQL 17 |
Framework | Power Modules Framework 2.1 |
CLI | Symfony Console 7.3 |
Container | Docker & Docker Compose |
Testing | PHPUnit 12.4 |
Code Quality | PHPStan 2.1, PHP CS Fixer 3.88 |
This project was intentionally built with custom implementations to demonstrate a deep understanding of async architecture patterns in PHP. Below is a comparison of what was implemented versus production-grade Symfony/ecosystem alternatives:
Custom Implementation | Production Alternative | Notes |
---|---|---|
Custom Command Queue (CommandQueue , Worker , CommandQueueRepository ) |
Symfony Messenger (symfony/messenger ) |
Messenger provides transport abstraction (AMQP, Redis, Doctrine), retry strategies, failure handling, and middleware pipeline. Custom implementation demonstrates understanding of queue mechanics, row locking, and exponential backoff. |
Custom DI Container (Power Modules Framework) | Symfony DependencyInjection (symfony/dependency-injection ) |
Symfony's DI offers autowiring, compiler passes, service tagging, and extensive configuration options. Power Modules was used to show modular architecture patterns. |
PostgreSQL LISTEN/NOTIFY (pg_notify in repository) | Redis Pub/Sub or Symfony Messenger with Redis Transport | PostgreSQL NOTIFY works but Redis is more suitable for high-throughput message passing. Demonstrates understanding of database-level notification mechanisms. |
Custom AbstractRepository (PDO-based with manual hydration) | Doctrine ORM (doctrine/orm ) |
Doctrine provides entity mapping, relationships, migrations, lazy loading, and query builder. Custom repository shows understanding of data mapping patterns and transaction management. |
Manual Serialization (serialize() /unserialize() ) |
Symfony Serializer (symfony/serializer ) |
Symfony Serializer supports JSON/XML/YAML, normalizers, and is more secure than native PHP serialization. Custom approach demonstrates message passing fundamentals. |
Custom Id Value Object (UUID v7 wrapper) | Symfony Uid Component (symfony/uid ) |
Symfony Uid provides Uuid, Ulid with built-in validation and factories. Custom implementation shows value object pattern and UUID v7 understanding. |
Custom Amount Value Object | Money PHP (moneyphp/money ) |
Money library handles currency conversion, formatting, allocation, and precise arithmetic. Custom implementation demonstrates understanding of financial data modeling. |
Manual Console Command Registration | Symfony Console AutoDiscovery with #[AsCommand] + CommandLoader |
Symfony can auto-register commands via service tags. Manual registration demonstrates understanding of console application lifecycle. |
Custom CompositeCommandHandler (manual handler registration) | Symfony Messenger Handler Locator with #[AsMessageHandler] |
Messenger uses service locator pattern with attribute-based discovery. Custom composite shows understanding of command/handler patterns. |
FOR UPDATE SKIP LOCKED (manual SQL) | Symfony Lock Component (symfony/lock ) or Messenger's native locking |
Symfony Lock provides store adapters (Redis, Database, Semaphore). Direct SQL demonstrates low-level concurrency control understanding. |
Why Custom Implementation?
- Demonstrates intimate knowledge of async patterns, database locking, and queue mechanics
- Shows ability to build production-grade features from scratch
- Proves understanding of the underlying concepts that frameworks abstract away
When to Use Symfony Components in Production?
- Symfony Messenger: Industry standard for async processing, battle-tested, extensive documentation
- Doctrine ORM: Rich feature set, community support, reduces boilerplate dramatically
- Symfony DI: Powerful autowiring and configuration management
- Symfony Serializer: Security, flexibility, and ecosystem integration
Trade-offs:
- Custom code = Full control + Educational value - Maintenance overhead - Missing ecosystem features
- Symfony components = Faster development + Community support + Rich features - Learning curve - Framework coupling
This project strikes a balance: it uses Symfony Console (industry standard) while implementing core async/queue logic from scratch to demonstrate architectural competence. In a production system, I would recommend Symfony Messenger with a Redis or RabbitMQ transport for the queue system, and Doctrine ORM for data persistence.
docker compose up -d
Option 1: Supervisor
[program:fqs-worker]
command=/app/bin/console command-queue:run-worker
process_name=%(program_name)s_%(process_num)02d
numprocs=3
autostart=true
autorestart=true
Option 2: Systemd
[Service]
ExecStart=/app/bin/console command-queue:run-worker
Restart=always
Option 3: Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: fqs-worker
spec:
replicas: 3
template:
spec:
containers:
- name: worker
command: ["php", "bin/console", "command-queue:run-worker"]
Note: Commands can be run either directly (if PHP is installed) or via Docker. Docker method works without PHP installed on the host system.
# Charge funds from an account (reason is optional)
docker compose exec worker-1 php bin/console trade:charge-funds <account-id> <units> <currency> [<reason>]
# Deposit funds to an account
docker compose exec worker-1 php bin/console trade:deposit-funds <account-id> <units> <currency>
# Transfer funds between accounts
docker compose exec worker-1 php bin/console trade:transfer-funds <source-account-id> <destination-account-id> <units> <currency>
# Place and capture holds
docker compose exec worker-1 php bin/console trade:place-hold <account-id> <units> <currency> <reason> [--expires-in-minutes=<minutes>]
docker compose exec worker-1 php bin/console trade:capture-hold <hold-id>
# All commands support optional --idempotency-key flag
docker compose exec worker-1 php bin/console trade:charge-funds <account-id> <units> <currency> <reason> --idempotency-key=<uuid>
Note:
units
is the amount in smallest currency units (e.g., cents for USD). So 5000 units = $50.00 USD.
Alternative: Direct execution (requires PHP 8.4+ installed)
bin/console trade:charge-funds <account-id> <units> <currency> [<reason>]
bin/console trade:deposit-funds <account-id> <units> <currency>
bin/console trade:transfer-funds <source-account-id> <destination-account-id> <units> <currency>
bin/console trade:place-hold <account-id> <units> <currency> <reason> [--expires-in-minutes=<minutes>]
bin/console trade:capture-hold <hold-id>
# Start a worker (used by Docker Compose, runs indefinitely)
docker compose exec worker-1 php bin/console command-queue:run-worker
# Or directly if PHP is installed:
# bin/console command-queue:run-worker
The system includes 2 pre-seeded test accounts (each with $1,000.00 USD):
Name | Account ID |
---|---|
John Doe | 0199dfbd-9da7-7e4c-8754-13fa5a9af89c |
Jane Smith | 0199dfbd-9da7-78b9-b132-8843318e7348 |
Console Commands β Command Queue (PostgreSQL)
β
ββββββββββββΌβββββββββββ
β β β
Worker 1 Worker 2 Worker 3
β β β
Command Handlers (Business Logic)
β β β
Domain Services & Repositories
- CommandQueue: PostgreSQL table with status tracking
- Worker: Polls queue, executes commands via handlers
- CompositeCommandHandler: Invokes all registered handlers; each handler checks if it supports the command type
- Repositories: Data access layer with transaction support
- Domain Models: Account, Transaction, User, Hold
# Run tests
make test
# Static analysis
make phpstan
# Code style check
make codestyle
# View logs
docker compose logs -f
# Database access
docker compose exec postgres psql -U fqs -d fqs
Service | Container Name | Purpose |
---|---|---|
postgres | fqs-postgres | PostgreSQL 17 database |
worker-1 | fqs-worker-1 | Command queue worker #1 |
worker-2 | fqs-worker-2 | Command queue worker #2 |
worker-3 | fqs-worker-3 | Command queue worker #3 |