We recently rolled out version 1 of our new B2B trade portal. On paper, the architecture was a practical compromise. Our legacy WooCommerce setup held over seven years of complex variation SKU mappings that we simply weren’t willing to manually rekey or migrate into a new system. By keeping WordPress as the back-end source of truth and building a decoupled, custom web application for the customer-facing portal, we avoided a high-risk database migration. We also saved our team of four back-office admins from having to learn a completely new inventory tool.
But split systems are messy. While decoupling solved our immediate frontend scaling concerns, it introduced logic drift and cache lag. We traded database migration pain for recurring coordination overhead. When you split an architecture like this, you aren’t just running two codebases; you are committing to keeping their hidden assumptions aligned.
Local environment friction and the CLI workaround #
wp-cli on its standard system path.
This blocked our QA team from running the initial database test seed, which stole a couple of hours from our test seeding sequence before we worked around it. Instead of rebuilding the local base image, we resolved it by spinning up a transient helper container to run our administrative tasks:
docker run --rm \
--volumes-from local-wordpress-container \
--network container:local-wordpress-container \
wordpress:cli wp user update admin-user --user_pass="temporary-dev-password"
It was an annoying detour. It showed us that local environments end up absorbing the same split-system friction as production, even for basic administrative tasks.
How our shipping business logic split in two #
We tracked this down to formula drift. Because we had built the frontend as a separate app, our custom WordPress API connector (Trade_Mapping_API::calculate_shipping()) ended up with its own replicated freight logic. That code fell out of sync when we updated our active core shipping plugin (slated_shipping). The API connector was still running an outdated formula of $140 base plus $30 per crate, while the real business logic had evolved. To fix this, we stopped trying to keep both versions in sync and refactored the API to delegate calculations to the main engine whenever it was active:
// In trade_mapping_plugin/includes/class-trade-mapping-api.php
if (class_exists('SSC_Shipping_Calculator')) {
$quote = SSC_Shipping_Calculator::calculate_quote($postcode, $items);
} else {
// Legacy fallback only if the primary calculator is missing
$quote = $this->run_legacy_formula($postcode, $items);
}
This fixed the calculation mismatch behind the scenes, but it left us with an awkward client-side problem. If a customer has their quote drawer open while the rate updates, the UI will still show the stale estimate until they refresh the page. We spent some time debating whether to build a polling mechanism to fix this, but ultimately decided to accept it as a known limitation for version 1. The trade-off felt reasonable: even if a user sees a stale price in their browser drawer, the final quote submission re-resolves the trusted price server-side at the moment of creation anyway. The business is safe, even if the UI can occasionally look slightly out of sync.
The high cost of stale pricing caches #
We initially relied on standard cache TTLs to clear out old data on the front-end. That was a mistake: in B2B commerce, a cached trade price running below the current wholesale rate is a margin exposure we can’t easily track after the fact.
To fix this, we built a direct cross-app cache invalidation bridge. We added a lightweight invalidation endpoint on the portal side (public/api/trade-cache-invalidate.php) and wrote our WordPress administrative plugin to ping it whenever an admin saves a record:
// Triggered in class-trade-mapping-admin.php on save or bulk update
$response = wp_safe_remote_post($portal_url . '/api/trade-cache-invalidate.php', [
'headers' => [
'Authorization' => 'Bearer ' . $shared_api_key,
'Content-Type' => 'application/json'
],
'body' => json_encode(['action' => 'clear_catalog_cache'])
]);
Now, when an admin uploads a CSV or remaps a SKU, the plugin immediately hits the API and clears the portal’s file-based catalog cache. Setting this up meant keeping track of an extra environment variable for the portal URL in our WordPress settings, but it eliminated the window between an admin update and a price-correct front end.
Dealing with version skew during rolling deployments #
Fatal error: Uncaught Error: Undefined constant Trade_Mapping_DB::OPTION_PORTAL_APP_URL
We were deploying a new portal URL setting field. The UI class (Trade_Mapping_Admin) had been updated to reference this new database key constant, but the database schema class (Trade_Mapping_DB) had not yet finished its update in that specific container’s PHP runtime. It was a classic version-skew bug.
To prevent transient errors during rolling deployments, I added a compatibility helper method in the UI class. It checks if the constant exists, and if not, gracefully falls back to the literal string key:
private static function get_portal_url_option() {
if (defined('Trade_Mapping_DB::OPTION_PORTAL_APP_URL')) {
return get_option(Trade_Mapping_DB::OPTION_PORTAL_APP_URL);
}
// Fallback during mixed-version deployments
return get_option('trade_portal_app_url');
}
This kind of defensive coding isn’t elegant, but it keeps the admin interface alive while our containers complete their rolling updates.
Was the hybrid path worth it? #
Despite the coordination headaches, the hybrid path was the right call for us. We delivered a fast, responsive checkout interface for our trade clients without touching the complex database mappings our back office relies on every day.
But we paid for that performance in coordination overhead. In retrospect, we underestimated how much time we would spend keeping our split logic in sync. If we were starting over, I would allocate a lot more time in the early sprints to design cache invalidation webhooks, handle version skew, and map out exactly where our business calculations should live. Splitting the system saved us from a massive migration, but it meant we had to become much more disciplined operators.