Case Study

Transaction Management Platform

Building a complete financial transaction management system for commodities trading, handling 15 distinct transaction types with external accounting integration and state-machine-based order tracking across 5+ years of production use.

Relevant for
2,504
Commits
105
DB Migrations
15
Transaction Types
5+
Years Active

The Challenge

A commodities trading business needed a unified platform to manage complex financial workflows across multiple currencies. The system had to handle 15transaction types including sales, purchases, deposits, withdrawals, credits, and specialised trading operations, each with distinct validation rules and business logic.

The platform needed to automate synchronisation with external accounting software, creating invoices and credit notes automatically whilst maintaining audit trails for compliance. Physical order fulfilment added another layer of complexity, requiring tracking through multiple delivery methods with state-specific validation.

Core requirements included precise currency handling to avoid floating-point errors, type-specific validation for transaction amounts (positive, negative, or zero depending on type), bi-directional accounting integration, and state-machine-based order tracking that managed physical goods across different delivery channels.

Show why transaction type validation is critical

Financial systems cannot treat all transactions the same. A deposit must have a positive amount. A withdrawal must be negative. Charges must be negative. Comments and adjustments can be zero. These aren't arbitrary rules, they encode fundamental business semantics that prevent data integrity errors.

Enforcing these rules at the entity level ensures validation happens regardless of how transactions are created, whether through API endpoints, bulk imports, or internal processes. The validation documentation became living business rule documentation, explaining why each transaction type exists and how it should behave.

Technical Architecture

The platform follows a clean layered architecture with clear separation of concerns. Controllers remain thin, handling only route management. Services orchestrate business logic. Factories centralise complex object creation. Repositories abstract data access. Entities enforce business rules through validation attributes.

Built with Symfony 6.4 and Doctrine ORM, the system uses modern PHP 8.1+ features including constructor property promotion, match expressions, attributes, and readonly properties. The Money library ensures precise currency handling, preventing floating-point errors common in financial applications.

2,504
Total Commits
70,831
Lines of PHP
836
Test Files
Show see the architectural decisions

Immutable Value Objects: Financial amounts use the Money library, ensuring precision and preventing floating-point errors. All currency handling goes through Money instances, never raw floats or decimals.

Entity-Level Validation: Business rules are enforced at the entity level using Symfony's validator component with custom constraint classes. This ensures data integrity regardless of how entities are created, whether through API calls, bulk imports, or internal processes.

Interface-Driven External Integrations: All external API calls are abstracted behind interfaces, allowing for testing with mocks and future provider changes without touching business logic.

Factory Pattern for Complex Creation: Transaction creation is centralised in factory classes that handle the complexity of setting up relationships, applying validation, and configuring defaults based on transaction type.

Technical Challenges

Building a platform that handles 15distinct transaction types presented several architectural challenges. Each type required different validation rules, different relationships to orders, and different export behaviours to accounting systems.

Challenge 1: Multi-Type Validation

Transaction types have fundamentally different amount sign requirements. Credits must be positive. Withdrawals must be negative. Comments can be zero. Transfers can be either positive or negative depending on direction. This business logic needed to be enforced at the database level, not just in API validation.

Solution: A complete validation system at the entity level using PHP 8 attributes, combined with a factory pattern that handles type-specific setup. The validator uses match expressions to map transaction types to allowed amount signs, with explicit documentation serving as living business rules.

Challenge 2: Accounting Integration

The platform must automatically create invoices and credit notes in external accounting software, attach PDF copies, and handle edge cases like duplicate invoice numbers, API failures, and different invoice types for different transaction categories.

Solution: A multi-layered approach with decision classes determining if/when to create external records, handler classes orchestrating the process, and solid error handling with detailed logging. OAuth token management automatically refreshes tokens before expiry, ensuring uninterrupted service.

Show how we solved invoice creation logic

Invoice creation isn't a simple "create invoice for every transaction" rule. The system must check: Is this a bulk import? (Don't create invoices.) Does an invoice already exist for this transaction? (Don't duplicate.) Is this an external transfer? (Don't export to accounting.) Does the order have items? (Can't invoice an empty order.) Is this a charge transaction that needs special handling?

Each check represents a real business requirement that emerged during development. The decision layer encapsulates this complexity in testable classes, keeping business logic separate from API orchestration. When invoice creation fails, the transaction still completes successfully with full error logging for later resolution.

Challenge 3: Order Fulfilment States

Physical precious metals orders require tracking through multiple states depending on delivery method: vault storage, courier delivery, or office collection. Each delivery method has its own valid state transitions, and the system must handle partial deliveries, payment status changes, and state transitions across different delivery types.

Solution: An embeddable value object encapsulates delivery type and status together with built-in validation ensuring only valid combinations. State transition logic is implemented using the State pattern, with each tracking state as a dedicated class implementing a common interface.

Implementation Highlights

Multi-Transaction Type Architecture

Managing 15transaction types required careful architecture. Type constants with semantic names provide clear documentation. A centralised transaction factory handles creation complexity. Dedicated services manage operations like splitting deposits into deposit and balance transactions.

Transaction types include: Sales Invoice (including vaulting charges), Purchase Order (buying from suppliers), Deposit (cash transferred in from clients), Withdrawal (cash sent to clients), Transfer (moving funds between currency accounts), Credit (customer credit notes), Charges (storage and handling fees), Comments (client-visible notes), External Transfer, Balance Adjustment, Sell-into-Repo, Buy-out-Repo, Adjustment, and Internal Comment (admin-only notes).

Show how the factory pattern scales

Each transaction type has its own factory method that handles type-specific setup: creating deposit transactions with vault ID lookup, creating order-linked transactions with proper deposit/balance splitting, creating transfer transactions with from/to account validation. This centralisation means business rules are enforced consistently regardless of creation path.

The factory pattern has scaled to handle 15 transaction types without significant refactoring. Adding a new transaction type means adding a factory method, defining validation rules, and updating the type enum. The architecture naturally guides developers towards the correct pattern.

External Accounting Integration

The integration layer handles OAuth token management with automatic refresh, invoice and credit note creation, line item mapping with proper account codes and tax handling, PDF attachment to accounting records, and resilient error handling that logs failures without blocking main transaction flow.

77commits dedicated to accounting integration, 114PHP files in the integration layer. The system has processed thousands of invoices with automatic retry, detailed logging, and graceful degradation when external services are unavailable.

Order Tracking & Fulfilment

The tracking system uses an embedded status value object combining delivery type and status with built-in validation. State pattern implementation provides dedicated classes for each tracking state. Payment status integration automatically updates tracking when orders are marked as paid. Delivery type transition mapping handles status conversion when delivery method changes.

71commits related to tracking, 87PHP files in the tracking subsystem. The system handles the complete lifecycle of physical order fulfilment with proper state validation, automatic payment integration, and detailed reporting capabilities.

Measurable Outcomes

The platform has been in continuous production use for over 5years, handling complex financial operations for a commodities trading business. The architecture has proven solid, maintainable, and extensible.

105
Database Migrations
53
Custom Validators
80+
Admin Controllers

Code quality metrics: Complete test suite with 836test files, PHPStan static analysis at maximum level with strict rules, PHP-CS-Fixer for consistent code style, PHPMD for complexity analysis, and GrumPHP pre-commit hooks ensuring quality gates.

Show what worked exceptionally well

Entity-Level Validation: Placing business rules directly on entities ensured they are always enforced, regardless of how data enters the system. This prevented numerous potential data integrity issues that would have been difficult to debug in production.

Type-Safe Currency Handling: The combination of strict typing, the Money library, and thorough validation resulted in zero reported currency calculation errors over 5+ years of production use.

Accounting Integration Resilience: The decision to handle external API failures gracefully (log and continue) rather than blocking transactions maintained business continuity during external service outages.

State Pattern for Tracking: Encapsulating state logic in dedicated classes made the tracking system maintainable despite growing complexity as business requirements evolved.

The platform demonstrates that well-architected domain models with careful separation of concerns can scale gracefully over multiple years of active development. The investment in proper abstractions, thorough validation, and clean architecture paid dividends in maintainability and reliability.

Related Services

How we can help you build similar platforms

Ready to eliminate your technical debt?

Transform unmaintainable legacy code into a clean, modern codebase that your team can confidently build upon.