Context
The client manufactures long-lived industrial equipment sold and serviced worldwide. Each unit is a capital good with a service life often measured in decades, carrying thousands of part numbers, consumables, and configuration options that the operator needs to be able to look up, price, and order without going through a salesperson.
The brief was a B2B platform that could carry that catalog: hundreds of thousands of SKUs, multi-organization access (one customer with multiple subsidiaries, each with its own roles and permissions), price lists that vary by customer / region / currency, an RFQ flow for items not on the standard catalog, and a configurator so an operator could specify which unit they were ordering parts for and have the catalog filter to compatible items.
The platform is built on OroCommerce Enterprise Edition 6.1 — Oro's flagship B2B-first e-commerce application on Symfony 6.4 / PHP 8.4. I joined as Lead Developer, owning the architecture and implementation of the Oro extension work that the stock platform doesn't cover out of the box.
Approach
OroCommerce EE is unusual among e-commerce platforms in being built explicitly around B2B requirements: native multi-organization, customer-specific pricing and visibility, RFQ / quote flows, and a Symfony-bundle-based extension model. That made it a strong base — but the brief still required customization across most of its subsystems.
The work was structured as a portfolio of 20+ custom Symfony bundles, each owning a coherent slice of behavior:
| Domain | Bundles |
|---|---|
| Catalog & search | Catalog API, WebCatalog, Equipment Configurator |
| Commerce | Pricing, Order, Checkout, Customer |
| Sales process | RFQ |
| Identity | Multi-Organization, custom auth layer |
| Fulfillment | Warehouse, Inventory, Shipping |
| Documents | Document |
| Analytics | Matomo integration |
Each bundle plugs into Oro's extension points — Symfony events, Doctrine listeners, Twig and YAML layout updates — rather than forking the platform. That keeps the door open to upstream Oro updates without three-way merge nightmares.
Implementation highlights
JSON:API by extending Oro's ApiBundle processors.
The platform needed headless integrations — partner systems pulling product data, pushing order events, syncing inventory. Rather than build a parallel API surface, I extended Oro\Bundle\ApiBundle's processor pipeline so the same domain models served the storefront and the JSON:API consumers, with field-level access controlled by Oro's existing ACL layer. New endpoints become a matter of registering a processor — not writing controllers.
Async workflows on RabbitMQ via Oro MessageQueue. Three pipelines run asynchronously over RabbitMQ:
- Catalog import — supplier feeds land as files, get parsed, normalized, and applied via idempotent batches so a re-run is safe. The same pipeline ingests machine technical sketches and machine-to-part mappings alongside parts data, backed by custom entities.
- Order processing — once a checkout completes, downstream side effects (ERP push, fulfilment notification, audit log) run as separate messages so a slow ERP doesn't block the checkout response.
- Inventory sync — incremental stock updates from the warehouse system stream in continuously and are applied with optimistic locking on the product entity.
All three run on Oro's MessageQueue components rather than a parallel Symfony Messenger setup, which keeps observability inside the same admin tooling Oro already provides.
Customer-specific pricing. B2B pricing is rarely "one price per SKU." Customers see negotiated prices, regional adjustments, and quantity breaks. I extended Oro's pricing engine via Doctrine listeners and Symfony events so the price-list resolution path stayed inside Oro's caching and indexing layer.
Per-customer / per-catalog product visibility — a custom three-layer model. Visibility was the hardest part of the brief: which customer may see which product, scoped per catalog, at production scale. Oro's stock customer-group resolver couldn't express it, so I replaced it with a three-layer model — catalog metadata → derived base-catalog permissions → a resolved product/scope table — enforced at both the Elasticsearch and the ORM query level. A customer never sees a part they're not entitled to whether they reach it via search or by navigating directly, and the resolution stays inside Oro's indexing layer rather than filtering in PHP after the fact.
Hierarchical equipment domain model. The catalog is navigated not as a flat SKU list but through a Machine / Assembly / Part / CustomerMachine hierarchy. An operator starts from the specific unit they own (CustomerMachine), drills into its assemblies, and reaches compatible parts — so the catalog filters itself to what fits their equipment instead of presenting the full ~140K-product list. Custom Doctrine entities back the hierarchy, populated by the import pipeline.
SAP Middleware integration. Inventory and order history live in the customer's SAP landscape, reached through a middleware layer. The integration runs a live inventory chain with a local fallback — a slow or unavailable SAP call degrades to cached stock rather than blocking the page — plus an order-history merge reconciling SAP order records with shop-side orders, and asynchronous PDF order confirmations delivered on dedicated MessageQueue topics.
Multi-organization with a custom authentication layer. A single corporate customer might have a dozen subsidiaries, each with its own users, its own catalog scope, and its own approval rules. The custom auth layer extends Oro's organization / business-unit model and translates the customer's external identity into the right Oro role and organization on every request, without persisting a sync.
Storefront machine viewer (Vue 3). A Vue 3 app embedded in the storefront maps products onto interactive machine sketches. The sidebar lists parts by material number and title; selecting an entry highlights the corresponding component on the sketch — turning "which part do I need?" from a part-number lookup into a visual one.
Search and storage.
- Elasticsearch 8 for product search and faceted catalog navigation.
- PostgreSQL 16 as the primary OLTP store.
- Redis for cache and session storage.
- MongoDB for document storage (technical drawings, datasheets, manuals).
CI/CD and quality. PHPUnit and Behat run in Jenkins on every push; PHPCS, PHP-CS-Fixer, and PHPMD are enforced as gate checks. Dockerized local environments mirror the staging deployment so onboarding a new developer takes hours, not days.
Outcome
Selected scale numbers from production:
- Catalog scale — ~140,000 products
- Configurator scope — 2,000+ catalogs and 800+ registered units
- Active organizations — ~1,000 customer organizations
- Build & deploy — Jenkins pipeline with PHPUnit + Behat gating every release
Architectural choices that paid off:
- The bundle-per-domain split kept feature work parallelizable; multiple bundles can ship in the same release without cross-coupling.
- Extending Oro's ApiBundle rather than building a parallel API surface saved an entire authorization layer.
- Running async workflows on Oro's MessageQueue meant ops and developers used the same dashboards rather than two stacks.
Stack
Backend — PHP 8.4, Symfony 6.4, OroCommerce EE 6.1, Doctrine ORM.
Storage & search — PostgreSQL 16, Elasticsearch 8, Redis, MongoDB.
Async — RabbitMQ via Oro MessageQueue.
Frontend — Vue 3, webpack 5.
Build & QA — Docker, Jenkins, PHPUnit, Behat, PHPCS, PHP-CS-Fixer, PHPMD.
API — JSON:API on top of Oro\Bundle\ApiBundle.