Three platforms, three implementations of the same business logic. Every cross-platform bug gets fixed three times. Every rule change gets implemented three times. Every regression gets debugged three times. If this sounds familiar, you're not dealing with a code quality problem. You're dealing with an architecture problem.

I've worked on two large-scale cross-platform systems where this was the starting point: a global car sales platform serving three continents, and a smart energy platform running on iOS, Android, and Web. In both cases, the teams were talented. The code was decent. But the architecture guaranteed inconsistency.

The duplication trap

When you build the same product for multiple platforms, the natural approach is to have a team per platform. iOS does their thing. Android does theirs. Web does the rest. Each team writes their own implementation of every business rule, validation, and workflow.

At first, this feels productive. Each team moves at their own pace, makes their own technical decisions, and ships on their own schedule. The problems start showing up around month three.

A pricing rule gets updated on Web but not on iOS. A validation message says one thing on Android and something different everywhere else. A new market launches and three teams need to implement the same requirements independently, each interpreting the specification slightly differently.

The bugs aren't random. They're structural. Your architecture is producing them reliably.

Why shared libraries fail

The first instinct is usually a shared library. Extract the common logic, put it in a package, import it everywhere. In theory this works. In practice it creates a different set of problems.

Shared libraries create coupling at the dependency level. Every platform needs to update the library in sync, or you end up with version drift that's even harder to debug than the original duplication. The library becomes a bottleneck: who owns it? Who reviews changes? Who decides when to release?

Worse, shared code libraries assume a shared runtime. A JavaScript validation function doesn't run natively on iOS. You end up either limiting yourself to the lowest common denominator or maintaining platform-specific wrappers around the shared code, which defeats the purpose.

The real issue isn't that the code needs to be shared. It's that the decisions need to be shared. What fields appear on this screen? What validation rules apply to this input? What options are available in this market? These are business decisions, not code.

Configuration-driven architecture

The pattern that actually works is to extract business logic into a declarative configuration layer. Instead of writing code that implements business rules on each platform, you write code that interprets a shared ruleset.

Think of it as separating the "what" from the "how." The configuration defines what should happen: which fields to show, which validations to run, which options to surface. Each platform decides how to render and execute that configuration using its native capabilities.

In practice, this usually looks like a JSON or YAML schema that describes:

  • UI structure: what screens exist, what fields they contain, how they're ordered
  • Validation rules: required fields, format constraints, conditional logic
  • Business rules: what's available in which market, what depends on what
  • Localization context: labels, messages, and formatting per locale

Each platform becomes an interpreter. iOS reads the same configuration as Android reads the same configuration as Web. When a business rule changes, it changes in one place. When a new market launches, it's a configuration change, not a development project.

Designing the ruleset

The hardest part isn't building the interpreter. It's designing the ruleset. Get this wrong and you end up with a configuration language that's as complex and brittle as the code it replaced.

A few principles that held up across the projects I've worked on:

Start concrete, abstract later. Don't try to build a universal configuration schema on day one. Start by looking at what your platforms actually do. Map out the screens, the fields, the rules. Look for the patterns. The configuration format should emerge from real requirements, not from a whiteboard exercise.

Keep the schema flat where possible. Deep nesting creates complexity that's hard to reason about and harder to debug. When a field technician reports that a configuration option isn't showing up on their tablet, you want to be able to look at the ruleset and see the answer in seconds, not trace through five levels of conditional inheritance.

Make the configuration human-readable. Someone on the business side should be able to look at the configuration and roughly understand what it says. If your configuration format requires a developer to interpret it, you've just moved the bottleneck rather than eliminating it.

Version the schema. Your configuration format will evolve. Old configurations need to keep working while new capabilities get added. Build versioning in from the start.

When to extract vs. when to leave

Not everything belongs in the configuration layer. The temptation is to extract everything, creating a configuration-driven system that tries to describe every possible interaction. This leads to a configuration language that's basically a programming language with worse tooling.

Extract into configuration when:

  • The logic is truly the same across platforms (validation rules, business constraints, data requirements)
  • Business stakeholders need to change it without developer involvement
  • The logic changes frequently or varies by market/region

Leave in platform code when:

  • It's genuinely platform-specific (gestures, native navigation patterns, accessibility APIs)
  • It involves complex interactions that a declarative format can't express cleanly
  • It changes rarely and doesn't vary across markets

The goal is to put the business decisions in configuration and leave the platform decisions in code. A clean separation here means business logic changes don't require platform releases, and platform improvements don't require business logic review.

Making the transition incrementally

You don't need to rewrite everything at once. In fact, you shouldn't. Both projects I worked on followed an incremental approach:

Pick a high-churn area. Find the part of your application where cross-platform bugs are most frequent and business rules change most often. This is where configuration-driven architecture will pay off fastest.

Build the interpreter on one platform first. Get it working on Web (or whichever platform iterates fastest), prove the configuration format works, then port the interpreter to other platforms. The configuration itself stays the same.

Migrate screen by screen. Don't try to convert the entire application in one sprint. Move one screen or feature area to configuration, validate it, then move the next. Each migration reduces your cross-platform bug surface immediately.

Measure what matters. Track cross-platform bugs before and after. On the Mercedes-Benz sales platform, cross-platform bugs dropped by 90% after moving to a JSON-based configuration system. On the Siemens inverter platform, the same approach enabled full feature parity across iOS, Android, and Web with a single source of truth.

What to watch out for

Configuration complexity creep. The configuration layer will want to grow. Every edge case will tempt someone to add another conditional, another override, another special case. Resist this. If your configuration starts needing its own test suite, you've gone too far.

Tooling gaps. Code has linters, type checkers, and IDEs. Configuration often has none of that. Invest early in validation tooling for your configuration format. A broken configuration should fail loudly at build time, not silently at runtime.

The "just this once" bypass. Someone will want to hard-code a rule on one platform because it's faster. It always starts with one exception. Within a quarter, you're back to maintaining divergent implementations. Make the configuration path the easy path.

Configuration-driven architecture isn't about eliminating platform-specific code. It's about making sure the decisions that should be consistent are consistent, by design rather than by discipline. When you separate what the product does from how each platform does it, cross-platform bugs stop being an inevitability and start being a rarity.