This post is part of the Monday weekly series where I write about my leadership playbook. Today’s post is Part-2 of last week’s post where I wrote about attempting to unify our architecture, codebase and products.
Our vision for a single, unified codebase was clear, but the journey from two established platforms to one cohesive system was less of a smooth road and more of an implementation minefield. The real work began when we had to confront the human and technical inertia built up over years of separate development.
Implementation Challenges and Strategic Solutions
We identified four core challenges that threatened to derail the project and developed specific strategies to overcome each one.
Challenge 1: Bridging Framework Differences
- The Problem: Our two legacy applications were built on distinct JavaScript frameworks with different paradigms, tooling, and ecosystems. We couldn’t just copy and paste code.
- The Solution: Choosing a Target and Rebuilding. We chose React as our target framework. This decision was driven by team expertise (more developers skilled in React), its thriving community and ecosystem, and its superior alignment with our emerging design system tools. Crucially, instead of attempting a direct component conversion (a high-risk, low-reward task), we decided to rebuild components using the new, shared design system principles. This ensured a clean slate and consistent quality from day one.
Challenge 2: The Pressure for Feature Parity
- The Problem: Stakeholders and product managers, wary of change, exerted significant pressure to ensure every single existing feature was preserved exactly as it was. This threatened to inflate the project scope dramatically.
- The Solution: Prioritized Migration List. We managed expectations by establishing a clear, three-tiered prioritization list for component migration:
- Must-Have: Core functionality essential for both versions.
- Should-Have: Features that significantly improved the user experience.
- Nice-to-Have: Edge cases and rarely-used functionality (which often ended up being simplified or retired).
- Communication Strategy: We countered feature creep with regular demos of the unified components, gathering feedback early to ensure alignment and build confidence in the new, superior experience.
Challenge 3: Addressing Customer Migration Concerns
- The Problem: Our customers, particularly those running the B2B solution on-premise, worried about forced, disruptive upgrades and feature losses.
- The Solution: Backward Compatibility and Gradual Rollout. We prioritized a backward compatibility strategy to mitigate risk:
- The old UI was maintained as a temporary fallback option.
- We initiated a gradual rollout with generous opt-in periods, allowing customers to move at their own pace.
- This was paired with clear communication about the long-term benefits and dedicated support during the transition period.
Challenge 4: Overcoming Team Resistance to Change
- The Problem: Engineers were comfortable and productive in the existing codebases, naturally resisting the effort of learning new patterns and migrating functional code.
- The Solution: A Change Management Approach. We treated the migration as an organizational change project, not just a technical one:
- Involvement: We involved engineers directly in architecture decisions, giving them ownership of the new system.
- Investment: We created structured learning opportunities and mentorship pairs to build new skills.
- Incentive: We celebrated early wins and shared success stories to maintain momentum and morale, making migration work a visible part of their career development plans.
The Unified Technical Architecture
Our architectural vision was designed to systematically eliminate duplication while retaining the necessary flexibility for two distinct deployment models.
1. Build-Time Configuration
We ensured deployment-specific behavior had zero runtime overhead by baking configuration into the build process.
// webpack.config.js
const config = {
resolve: {
alias: {
'@config': path.resolve(__dirname, `src/config/${DEPLOYMENT_TYPE}`)
}
}
}
This pattern allowed us to import the correct configuration files or components based on the target deployment type during compilation.
2. Feature Flagging System
For runtime control over feature visibility, we implemented a lightweight feature flagging system within the application shell.
// FeatureGate component
const FeatureGate = ({ feature, deployment, children }) => {
const isEnabled = featureConfig[deployment]?.[feature];
return isEnabled ? children : null;
}
This provided a clean, component-level way to handle deployment-specific feature availability.
3. Shared State Management
We established a single source of truth for application data that was aware of its execution context.
// Redux store with deployment-aware reducers
const store = createStore(
rootReducer,
{
deployment: DEPLOYMENT_TYPE,
features: getFeatureConfig(DEPLOYMENT_TYPE)
}
)
The application state adapted immediately to the deployment context (B2C or B2B), ensuring components rendered correctly without conditional logic throughout the code.
In the next week’s post I will wrap up this 3 part series of unification and share the success metrics we defined and key lessons learned.






Leave a comment