Enterprise B2B/B2C Multi-Region Magento Platform
How we built a sophisticated multi-region e-commerce platform serving six storefronts across the UK, USA, Ireland, and Europe with complex ERP integration, automated synchronisation, and 201 integration tests.
Multi-Region E-Commerce at Scale
Managing 6 storefrontsmarket segments across multiple regions while synchronising with legacy ERP systems creates serious architectural challenges. This platform shows how to build maintainable, testable e-commerce systems at scale. It processes orders from five concurrent sales channels while maintaining data integrity across two ERP systems and multiple marketplace integrations.
The system handles bidirectional SOAP synchronisation through 27,000 linesERP integration. It uses proven patterns for multi-region data handling, complex shipping logistics, and B2B/B2C pricing segmentation. Three layers form the core architecture: storefront isolation, region-aware processing, and automated synchronisation.
Show platform scope and complexityHide platform scope and complexity
- Six storefronts: UK B2B, USA B2B, USA D2C, UK B2C, Ireland, and European markets with independent pricing and tax configurations
- Three ERP endpoints: UK, US, and European SOAP APIs with different WSDL schemas requiring region-specific data transformations
- Seven warehouses: Region-specific stock management with automated routing based on customer location and product availability
- Multiple sales channels: Direct e-commerce sites plus TikTok, OnBuy, and Temu marketplaces, all processed through a single fulfilment pipeline
- Dual pricing models: Wholesale carton quantities with minimum order increments vs retail single-unit sales with VAT-inclusive pricing
- Payment gateways: Worldpay, BlueSnap, and Brippo integration with region-specific configurations
These architectural requirements drove specific technology choices. The platform needed modern PHP features for type safety, proven frontend performance, and robust developer tooling to maintain quality across 87,000 lines of code.
Show technical stack decisionsHide technical stack decisions
The platform runs on Magento 2.4.7-p3 with PHP 8.3, leveraging strict types, match expressions, and union types throughout. Hyva Theme was selected for modern frontend development and performance characteristics, whilst maintaining Luma checkout for complex B2B workflows.
Development environment uses Warden (Docker-based) for consistent local environments across the team. Elasticsearch 7 powers search with Mirasvit extensions for advanced scoring rules.
PIM integration via Akeneo provides single source of truth for product data, with automated synchronisation keeping Magento catalogues aligned with master product information.
Ready to eliminate your technical debt?
Transform unmaintainable legacy code into a clean, modern codebase that your team can confidently build upon.
Architecture Overview
The system has four distinct layers, each handling specific concerns while maintaining clean separation. The storefront layer manages six independent sales channels. The application layer provides store-aware business logic. The integration layer handles external systems, and the automation layer orchestrates scheduled operations.
Show architectural layers explainedHide architectural layers explained
Storefront Layer: Six websites (UK B2B, USA B2B, USA D2C, UK B2C, Ireland, Europe) with independent catalogues, pricing, and checkout flows. Each storefront has its own domain, SSL certificate, and theme customisation whilst sharing core business logic.
Application Layer: Store-aware plugins intercept pricing, quantity validation, and configuration at key touchpoints. Shipping resolver applies carrier rules based on website, country, postcode, and product dimensions. Pricing segmentation ensures wholesale and retail prices never leak between customer groups.
Integration Layer: ERP module (27,000 lines) handles bidirectional SOAP synchronisation with Business Central. Import framework (20,000 lines) processes products, customers, and inventory with sanity checks. Marketplace connectors (M2E Pro) handle TikTok, OnBuy, and Temu.
Automation Layer: Cron orchestrator with load-aware scheduling, collision prevention, and failure alerting. Manages 26 jobs including ERP sync, index maintenance, and marketplace exports.
To address these challenges, we implemented a pattern-based solution using PHP 8 match expressions. The router pattern makes regional differences explicit whilst maintaining type safety through the PHP compiler.
This architectural approach might seem counterintuitive. Why not abstract the regional differences behind a unified interface? The answer lies in the trade-offs between abstraction complexity and explicit regional handling.
Show why not a unified ERP endpoint?Hide why not a unified ERP endpoint?
The legacy ERP system (Business Central) was deployed regionally with different schemas and authentication mechanisms per region. UK uses NTLM authentication with one WSDL schema, US uses different field names and data types, and European endpoint has additional VAT handling fields.
A unified adapter would have hidden important regional differences in a complex transformation layer. The router pattern makes regional differences explicit and type-safe through the PHP type system instead.
When the US WSDL changed to add new shipping fields, only the US implementation needed updating. The UK and European implementations remained untouched, reducing risk of regression.
Technical Challenges
Multi-Region ERP Synchronisation
The legacy ERP system exposes three separate SOAP endpoints with different WSDL schemas, authentication mechanisms, and data formats. Routing orders to the correct endpoint, transforming data between formats, and handling region-specific business rules without creating a maintenance nightmare was tricky.
Show the synchronisation challengeHide the synchronisation challenge
The ERP integration needs to handle 12 entity types (orders, customers, products, shipments, invoices, and more) across three regions, meaning 36 distinct WSDL files to manage. Each region has subtle differences in field names, data types, and validation rules.
For example, the UK WSDL uses CustomerGroup whilst the US uses CustomerClass. European orders require VAT registration numbers that don't exist in US orders. All three endpoints require NTLM authentication with different service accounts.
Bidirectional synchronisation means changes in Magento trigger ERP updates, and ERP changes trigger Magento imports. The system must track synchronisation state, handle conflicts, and retry failures without duplicating data.
Beyond routing, the system needed to protect business logic from the volatility of generated SOAP code. When WSDL schemas change during ERP upgrades, a single abstraction layer prevents cascading changes throughout the application.
The wrapper handles schema differences, but converting Magento orders to ERP format requires multiple transformation stages. Each stage handles one concern independently, enabling composition-based flexibility across regions.
Show converter pipeline architectureHide converter pipeline architecture
Converting Magento orders to ERP format requires multiple specialised converters chained together. Each converter handles one concern (dates, addresses, payment, shipping, currency) and can be tested independently.
The pipeline pattern uses composition over inheritance, allowing converters to be reused across regions or swapped based on business rules. For example, payment method conversion differs between B2B (account terms) and B2C (card payments), but both share address conversion logic.
Error handling follows graceful degradation: non-critical failures (like shipping lookup misses) return null with logging rather than throwing exceptions. This prevents a missing shipping rule from blocking order export entirely whilst still alerting operations to fix the rule.
Complex Shipping Logic
Shipping carrier selection depends on website, country, postcode pattern, product dimensions, and priority flags. Orders may contain mixed items requiring different carriers. Four database tables manage shipping rules: carrier mapping, order filters, order maps, and packaging items. We needed to resolve the correct carrier without overwhelming checkout logic.
Show why shipping resolution is complexHide why shipping resolution is complex
The business operates multiple warehouses with region-specific carrier contracts. UK orders might use Royal Mail, DPD, or Parcelforce depending on dimensions and destination. US orders route through USPS, UPS, or FedEx based on similar criteria.
Some postcodes require specific carriers due to delivery restrictions (islands, highlands, rural areas). Priority orders override standard routing. Oversized items require freight carriers regardless of destination.
The complexity comes from orders containing mixed items: standard products, oversized items, and fragile goods. The resolver must determine whether to split shipments or find a carrier capable of handling all items together.
Shipping Resolution Pipeline
Understanding the complexity is one thing. Implementing a maintainable solution is another. The resolver uses a five-stage pipeline that optimises common cases whilst correctly handling edge scenarios.
Show how shipping resolution worksHide how shipping resolution works
The system implements a five-stage resolution pipeline:
- 1. Exclusion check: Orders on the exclusion list skip automated carrier selection and require manual fulfilment (custom orders, special handling requirements)
- 2. Item packaging: Determine package type per item (envelope, small parcel, large parcel, freight) based on dimensions and weight from product attributes
- 3. Lookup type determination: Single-item orders and homogeneous multi-item orders use simpler ALL lookup. Heterogeneous orders trigger complex PART resolution to find compatible carriers
- 4. Rule application: Match against carrier mapping database using website, country, postcode pattern, and package types. Wildcard postcodes (* suffix) enable zone-based rules
- 5. Postcode override: Final check for destination-specific overrides (island postcodes forcing specific carriers regardless of package type)
This pipeline architecture needed concrete implementation. The resolver balances performance with flexibility using wildcard pattern matching and lookup optimisations for the most common order types.
B2B/B2C Pricing Segmentation
Wholesale customers see carton pricing with quantity increments (minimum 24 units); retail customers see single-unit pricing with VAT included. Both must coexist in the same catalogue without price leakage between segments or customer confusion during checkout.
Show the price segmentation challengeHide the price segmentation challenge
B2B customers purchase in wholesale quantities (cartons of 24, pallets of 96) at discounted trade prices. B2C customers purchase single units at retail prices including VAT. The same product must appear with different pricing and quantity rules depending on which storefront the customer visits.
Naive approaches leak prices: B2C imports might overwrite B2B prices if not scoped correctly, B2B customers might see VAT-inclusive prices from B2C stores, or retail customers might encounter minimum order quantities designed for wholesale.
Promotional pricing makes this harder. B2B customers receive volume discounts and account-specific pricing. B2C customers see promotional banners and time-limited offers. These pricing layers must remain independent while sharing the same product catalogue and import processes.
Solving price segmentation required intercepting Magento's pricing engine at multiple touchpoints. Plugins provide a clean way to inject store-specific behaviour without modifying core business logic.
With plugins intercepting price operations, determining the current store context efficiently becomes critical. The solution caches store type detection using PHP 8 match expressions to avoid redundant queries in hot code paths.
Show store detection patternHide store detection pattern
We cache store type in an instance variable using PHP 8 match expressions instead of querying the store manager on every request. The match expression evaluates once. Subsequent calls return the cached result. O(1) lookups in request-heavy code paths like price rendering.
The pattern uses Magento's store manager to detect the current store code, then matches against known B2C codes. The default case assumes B2B, ensuring new stores are wholesale by default unless explicitly configured as retail.
This pattern appears throughout the codebase: shipping calculation, tax configuration, and checkout validation all query store type. Caching eliminates redundant store manager calls whilst maintaining accurate store detection.
Ready to eliminate your technical debt?
Transform unmaintainable legacy code into a clean, modern codebase that your team can confidently build upon.
Implementation Highlights
Integration Testing at Scale
Getting meaningful test coverage on a platform with complex ERP integration, multiple storefronts, and intricate shipping rules needed a comprehensive testing framework built on modern Magento patterns. We built 201 integration tests covering critical business logic, from shipping resolution to price segmentation to ERP data transformation.
Show why integration tests over unit tests?Hide why integration tests over unit tests?
Unit tests excel at testing isolated logic without external dependencies. But e-commerce platforms have complex interactions between database, Magento core, and custom modules. Unit tests with extensive mocking often test the mocks rather than actual system behaviour.
Integration tests run against a real Magento installation with database, allowing tests to verify actual data flow. Shipping resolution isn't just logic, it queries database tables. Price calculations interact with Magento's pricing engine. ERP converters transform complex nested objects.
The trade-off is test speed (integration tests are slower) and complexity (fixtures require database setup). But the confidence gained from testing real system behaviour outweighs these costs for critical business logic.
Our testing strategy: never mock DTOs. DTOs are simple data carriers with no behaviour to mock. Testing with real DTOs validates actual data flow, while mocking them only tests the mock's behaviour. This approach caught several serialisation bugs that mocks would have hidden.
Beyond modern PHPUnit attributes, the fixture system itself needed careful architecture. Reusable fixtures reduce boilerplate whilst parameterisation enables flexible test scenarios across different configurations.
Show fixture architectureHide fixture architecture
The 137 custom fixtures implement Magento's DataFixtureInterface, providing consistent test data setup. Each fixture is reusable across tests, dramatically reducing test setup boilerplate.
Fixtures support parameterisation via attributes, allowing the same fixture to create different variants. For example, CarrierMappingFixture accepts website, country, postcode pattern, and carrier code parameters, enabling tests to create specific shipping scenarios.
The DbIsolation attribute wraps tests in database transactions, ensuring each test starts with clean state. This prevents test pollution where one test's data affects another, eliminating an entire class of flaky test failures.
Intelligent Cron Orchestration
Managing 26scheduled jobs across imports, exports, indexing, and marketplace sync without overwhelming server resources or causing data corruption from concurrent writes needed a four-layer protection system with dynamic load awareness. We built 814 lines of bash automation. Zero production incidents from job overload since deployment.
Show the cron collision problemHide the cron collision problem
Magento cron jobs can run for unpredictable durations based on data volume. Product imports might take 5 minutes with 100 new products or 45 minutes with 10,000 updates. If jobs aren't collision-protected, multiple instances run simultaneously, fighting for database locks and potentially corrupting data.
Simple solutions like "don't schedule jobs too frequently" fail at scale. ERP imports need to run hourly for fresh inventory data. Indexing must follow imports. Marketplace exports need high frequency for order fulfilment. These requirements create complex scheduling interdependencies.
Server load spikes from traffic or other processes can slow jobs unpredictably. A job that normally completes in 10 minutes might take 30 minutes during peak load. Static scheduling can't adapt to these dynamic conditions.
Understanding the collision problem led to a four-layer protection system. Each layer addresses different failure modes, from system resource constraints to process-level race conditions.
The protection layers provide collision prevention, but effective scheduling also requires strategic timing offsets. Staggering related jobs ensures sequential execution whilst maintaining synchronisation requirements.
Show scheduling strategy detailsHide scheduling strategy details
- ERP imports staggered: Products import at :00, Customers at :15, Discounts at :30, Shipments at :45. This prevents all imports competing for resources simultaneously whilst maintaining hourly synchronisation.
- Indexing jobs offset: 2-minute intervals between reindex operations ensure one completes before the next starts. Catalogue search, price index, and inventory all run sequentially.
- High-frequency exports: Order exports run every 10 minutes with memory limit override (12GB) to handle large order batches. Skip execution if previous export still running.
- Failure alerting: Slack notifications include full diagnostic context (log tail, load average, memory stats, process list). Operations can diagnose issues without SSH access.
This approach might seem unusual. Why add bash wrappers when Magento has built-in cron handling? The decision comes down to separation of concerns and upgrade safety.
Show why bash instead of Magento cron?Hide why bash instead of Magento cron?
Magento's built-in cron system handles job scheduling, but it lacks sophisticated collision prevention and load awareness. Bash wrappers add protective layers without modifying Magento core, keeping the solution upgrade-safe.
The bash layer sits between system cron and Magento cron, providing infrastructure-level protections that apply to all jobs uniformly. This is simpler than implementing collision detection in each Magento job class.
Bash scripts can access system resources (load average, memory, process table) more easily than PHP. The wrapper pattern also enables job-specific memory limits and timeout configurations without modifying Magento configuration.
Outcomes & Results
What Worked Well
- Region Router Pattern: Clean separation of UK/US/EU logic without code duplication. Adding a new region requires implementing the interface, not modifying existing code. Exhaustive match expressions ensure compiler catches missing cases.
- DTO Wrappers: Insulating business logic from generated SOAP code proved invaluable when WSDL schemas changed. Only wrapper methods needed updating, protecting 27,000 lines of business logic from schema churn.
- Load-Aware Automation: Zero production incidents from cron job overload since implementing dynamic load detection. Jobs gracefully skip execution during high-load periods rather than failing.
- Fixture-Based Testing: 137 reusable fixtures dramatically reduced test setup time from hours to minutes and improved test clarity through declarative dependencies.
Show what could be improvedHide what could be improved
Configuration Complexity: Six websites means six sets of configuration to maintain manually. A configuration inheritance system would reduce duplication, allowing base configurations with store-specific overrides.
Shipping Rule Management: Rules stored in database tables require developer intervention to modify. An admin interface would improve operational efficiency, allowing business users to adjust carriers without deployments.
Monitoring Depth: Slack notifications provide failure alerts but lack historical metrics. Adding structured logging to a time-series database would enable trend analysis and proactive issue detection.
Key Lessons
- Match expressions over if-else: PHP 8 match expressions make region routing exhaustive and self-documenting. The compiler ensures all cases are handled, catching missing regions at build time rather than runtime.
- Never mock DTOs: Simple data objects should be instantiated with real data, not mocked. Mocking them tests the mock's behaviour rather than actual data flow. This caught serialisation bugs mocks would have hidden.
- Graceful degradation over exceptions: For non-critical failures (shipping lookup misses), returning null with comprehensive logging is preferable to throwing exceptions that block order processing.
- Random jitter prevents thundering herd: A simple 1-10 second random delay before process checks eliminated observed scheduling collisions in production when multiple jobs start simultaneously.
- Composition over inheritance: Converters compose specialised transformers rather than inheriting shared behaviour, improving testability and allowing converters to be mixed and matched across regions.
Show skills demonstrated in this projectHide skills demonstrated in this project
Enterprise Integration: SOAP/WSDL integration with NTLM authentication, multi-region API routing and data transformation, bidirectional ERP synchronisation with conflict resolution, generator patterns for memory-efficient large dataset processing.
E-Commerce Domain: Multi-website Magento architecture with store-scoped configuration, B2B/B2C pricing segmentation with three protection layers, complex shipping rule engines with wildcard postcode matching, marketplace integration (TikTok, OnBuy, Temu) through unified fulfilment pipeline.
Testing Excellence: Modern PHPUnit attribute-based fixtures with declarative dependencies, database isolation strategies preventing test pollution, real DTO testing with never-mock philosophy, comprehensive integration test coverage (201 tests, 17,307 lines).
DevOps and Automation: Load-aware job scheduling with dynamic thresholds, four-layer collision prevention (flock, process checks, jitter, staggering), failure alerting with full diagnostic context (logs, load, memory), staggered scheduling strategies for complex dependencies.
Code Quality: PHP 8 match expressions and union types for exhaustive handling, repository pattern with interface segregation, composition over inheritance for converter pipeline, British English coding standards enforcement, strict type declarations throughout.
These patterns and techniques aren't specific to this one platform. The architectural approaches apply broadly across different industries and technical contexts.
Show applicability to other projectsHide applicability to other projects
Wholesale Distribution: Multi-region ERP integration patterns apply directly to any business synchronising with Business Central, SAP, or similar regional deployments. The DTO wrapper and region router patterns work for any multi-endpoint API.
Multi-Brand Retail: The store-aware plugin architecture supports any scenario requiring different pricing, taxation, or business rules per brand. Magento's multi-store capabilities combined with scoped configuration enable brand isolation.
International E-Commerce: Region-specific shipping, currency, and tax handling patterns are universally applicable. Warehouse-to-region mapping and carrier selection logic translate to any multi-region fulfilment operation.
Automated Operations: Load-aware cron orchestration is valuable for any system with resource-intensive scheduled jobs. The four-layer protection pattern prevents collision issues in any job scheduling system.
Relevant Industries
Industries that benefit from these patterns
Ready to eliminate your technical debt?
Transform unmaintainable legacy code into a clean, modern codebase that your team can confidently build upon.