Remember when we thought we’d cracked the code? Back in 1995, Sun Microsystems boldly proclaimed “Write Once, Run Anywhere” (WORA) as Java’s superpower. Fast forward to 2025, and we’re still chasing that same elusive dream with React Native, Flutter, and a parade of frameworks promising to be the “one framework to rule them all.” Spoiler alert: we’re still debugging everywhere. Let me be brutally honest here – after years of wrestling with cross-platform development, watching projects spiral into maintenance nightmares, and seeing developers pull their hair out over platform-specific quirks, I’ve come to a controversial conclusion: WORA is not just oversold; it’s fundamentally flawed as a philosophy. But before you grab your pitchforks, hear me out. This isn’t about bashing cross-platform development entirely. It’s about understanding why the promise falls short and how we can do better.
The Seductive Promise That Never Quite Delivers
The appeal is undeniably intoxicating. Picture this: you write your code once, press a magical deploy button, and voilà – your app runs perfectly on iOS, Android, web, Windows, macOS, and probably your smart toaster too. No more maintaining separate codebases, no more platform-specific bugs, no more explaining to stakeholders why the iOS version costs twice as much as initially quoted. Modern frameworks have certainly made this dream more tangible:
- React Native with React Native Web – Share code between mobile and web
- Flutter – Google’s Dart-powered solution for multiple platforms
- Kotlin Multi-Platform – JetBrains’ take on code sharing
- Ionic – Web technologies wrapped in native containers These tools genuinely reduce code duplication and can accelerate development. But here’s where reality crashes the party like an uninvited relative at Thanksgiving dinner.
The Harsh Reality: Platform Differences Are Features, Not Bugs
Let’s dive into a real-world example. Imagine you’re building a simple file picker component. Sounds straightforward, right?
// The idealistic WORA version
function FilePicker({ onFileSelect }) {
const handleFileSelection = () => {
// This should "just work" everywhere, right?
const file = selectFile();
onFileSelect(file);
};
return (
<TouchableOpacity onPress={handleFileSelection}>
<Text>Pick a File</Text>
</TouchableOpacity>
);
}
Now, let’s see what this “simple” component actually needs to handle across platforms:
// The brutal reality
import { Platform } from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import { launchImageLibrary } from 'react-native-image-picker';
function FilePicker({ onFileSelect, fileType = 'any' }) {
const handleFileSelection = async () => {
try {
if (Platform.OS === 'web') {
// Web implementation
const input = document.createElement('input');
input.type = 'file';
input.accept = getWebAcceptString(fileType);
input.onchange = (e) => onFileSelect(e.target.files);
input.click();
} else if (Platform.OS === 'ios') {
// iOS has different permissions and UX patterns
const result = await DocumentPicker.pickSingle({
type: getIOSDocumentTypes(fileType),
copyTo: 'documentDirectory' // iOS-specific requirement
});
onFileSelect(result);
} else if (Platform.OS === 'android') {
// Android handles file access differently
const result = await DocumentPicker.pickSingle({
type: getAndroidMimeTypes(fileType),
// Android doesn't need copyTo in most cases
});
onFileSelect(result);
}
} catch (error) {
if (DocumentPicker.isCancel(error)) {
// Handle cancellation - but wait, web doesn't throw this!
if (Platform.OS !== 'web') {
console.log('User cancelled file selection');
}
} else {
// Platform-specific error handling
handlePlatformSpecificError(error);
}
}
};
// Different UI components needed for different platforms
if (Platform.OS === 'web') {
return (
<button onClick={handleFileSelection} className="file-picker-btn">
Choose File
</button>
);
}
return (
<TouchableOpacity
onPress={handleFileSelection}
style={Platform.select({
ios: styles.iosButton,
android: styles.androidButton,
})}
>
<Text style={Platform.select({
ios: styles.iosText,
android: styles.androidText,
})}>
Pick a File
</Text>
</TouchableOpacity>
);
}
Suddenly, our “write once” component has become a maze of platform-specific conditionals. And this is just file picking – imagine handling camera access, push notifications, or deep linking!
The Technical Reality: Why WORA Fails
The fundamental issue isn’t with the frameworks themselves – they’re remarkable engineering achievements. The problem lies in the assumption that platforms are just different skins on the same underlying system.
Operating System Differences Run Deep
Even something as basic as file paths becomes a headache:
# Python example of cross-platform file handling
import os
import platform
def get_config_path():
if platform.system() == "Windows":
return os.path.join(os.environ['APPDATA'], 'MyApp', 'config.json')
elif platform.system() == "Darwin": # macOS
return os.path.expanduser('~/Library/Application Support/MyApp/config.json')
else: # Linux and others
return os.path.expanduser('~/.config/myapp/config.json')
# What we wished we could write:
# return get_universal_config_path() # This doesn't exist
User Experience Expectations Vary Wildly
iOS users expect smooth animations and gesture-based navigation. Android users are accustomed to hardware back buttons and Material Design patterns. Web users think in terms of URLs and browser controls. Desktop users want keyboard shortcuts and resizable windows. A truly cross-platform app that ignores these differences feels foreign on every platform – the dreaded “uncanny valley” of user interfaces.
The Docker Delusion: Containers Don’t Solve Everything
“But what about containers?” I hear you ask. Docker promised to solve the deployment side of WORA, but it brings its own set of limitations:
# This Dockerfile works great... on Linux
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# But good luck running this Windows container on Linux:
# FROM mcr.microsoft.com/windows/nanoserver:1809
# This simply won't work on a Linux host
Containers are OS-dependent. You can’t run a Windows container on Linux, and platform-specific dependencies still creep in through native modules, system calls, and hardware interactions.
A Better Philosophy: Strategic Code Sharing
Instead of forcing everything into a single codebase, successful cross-platform projects embrace strategic code sharing. Here’s what this looks like in practice:
Shared Business Logic Layer
// shared/userService.ts - Platform agnostic
export class UserService {
private apiClient: ApiClient;
constructor(apiClient: ApiClient) {
this.apiClient = apiClient;
}
async authenticateUser(credentials: LoginCredentials): Promise<User> {
const response = await this.apiClient.post('/auth/login', credentials);
return this.mapApiResponseToUser(response);
}
async validateEmail(email: string): Promise<boolean> {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
private mapApiResponseToUser(response: any): User {
// Complex mapping logic shared across platforms
return {
id: response.user_id,
name: response.full_name,
email: response.email_address,
// ... more mapping
};
}
}
Platform-Specific UI Implementations
// ios/LoginScreen.tsx
export function LoginScreen() {
const userService = new UserService(createIOSApiClient());
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView behavior="padding">
{/* iOS-specific UI components */}
<IOSTextInput placeholder="Email" />
<IOSButton title="Login" onPress={handleLogin} />
</KeyboardAvoidingView>
</SafeAreaView>
);
}
// android/LoginScreen.tsx
export function LoginScreen() {
const userService = new UserService(createAndroidApiClient());
return (
<View style={styles.container}>
{/* Android-specific UI components */}
<MaterialTextInput label="Email" />
<MaterialButton onPress={handleLogin}>Login</MaterialButton>
</View>
);
}
Practical Steps for Sane Cross-Platform Development
1. Start with Platform-Specific Prototypes
Before jumping into cross-platform frameworks, build small prototypes on each target platform. This reveals the unique challenges and opportunities of each environment.
# Create focused prototypes
mkdir prototypes
cd prototypes
npx create-expo-app mobile-prototype
npx create-react-app web-prototype
# Test core functionality on each platform
2. Identify What Can Actually Be Shared
Not everything should be shared. Create a decision matrix:
Component Type | Shareable | Platform-Specific | Reason |
---|---|---|---|
API calls | ✅ | ❌ | Network requests are universal |
Data validation | ✅ | ❌ | Business rules don’t change |
Navigation patterns | ❌ | ✅ | Each platform has conventions |
Animations | ❌ | ✅ | Performance characteristics differ |
File handling | ❌ | ✅ | OS APIs vary significantly |
3. Design Your Architecture for Platform Differences
// Define platform abstractions upfront
interface PlatformBridge {
showNotification(message: string): Promise<void>;
pickImage(): Promise<ImageFile>;
storeSecurely(key: string, value: string): Promise<void>;
}
// Implement per platform
class IOSPlatformBridge implements PlatformBridge {
async showNotification(message: string): Promise<void> {
// iOS-specific notification implementation
UNUserNotificationCenter.current().requestAuthorization();
// ... iOS notification code
}
}
class AndroidPlatformBridge implements PlatformBridge {
async showNotification(message: string): Promise<void> {
// Android-specific notification implementation
PushNotification.localNotification({
title: "App Notification",
message: message,
});
// ... Android notification code
}
}
4. Embrace Platform-Specific Optimizations
Instead of fighting platform differences, leverage them:
// Leverage platform strengths
const Camera = () => {
if (Platform.OS === 'ios') {
// iOS has excellent native camera APIs
return <IOSNativeCamera onCapture={handleCapture} />;
} else if (Platform.OS === 'android') {
// Android's camera2 API offers more control
return <AndroidCamera2Component onCapture={handleCapture} />;
} else {
// Web uses getUserMedia
return <WebCameraComponent onCapture={handleCapture} />;
}
};
The Uncomfortable Truth About Maintenance
Here’s something nobody talks about: cross-platform apps often become harder to maintain, not easier. You end up with a codebase that’s part JavaScript, part Swift, part Kotlin, part CSS, with a sprinkling of platform-specific configuration files and build scripts. When a bug appears on iOS but not Android, you’re not just debugging your code – you’re debugging the framework’s abstraction layer, the build process, and potentially the underlying platform bindings. I’ve seen teams spend more time wrestling with React Native’s Metro bundler and Gradle configurations than they would have spent building separate native apps.
When WORA Actually Makes Sense
Despite my criticisms, there are scenarios where cross-platform development shines:
MVPs and Proof of Concepts
When you need to validate an idea quickly across platforms, cross-platform tools are invaluable.
Internal Tools and CRUD Applications
If you’re building internal dashboards or basic data management apps, the UI expectations are lower and the business logic is more valuable than platform-specific polish.
Teams with Limited Platform Expertise
If you have a strong web development team but limited iOS/Android experience, React Native might be your fastest path to market.
The Future: Embracing Hybrid Approaches
The most successful cross-platform projects I’ve seen don’t try to do everything in one framework. They use:
- Shared backend services for business logic
- Shared design systems for consistency
- Platform-specific frontend implementations for optimal UX
- Cross-platform frameworks for specific components where it makes sense
Conclusion: It’s Not About Perfection, It’s About Pragmatism
WORA isn’t inherently evil – it’s just oversold. The promise of writing code once and running it everywhere appeals to our desire for simplicity in an increasingly complex world. But software development has never been simple, and platform differences exist for good reasons. Instead of chasing the impossible dream of perfect code portability, we should focus on smart code sharing strategies that respect platform differences while maximizing developer productivity. The next time someone pitches you a framework that claims to solve all your cross-platform woes, ask them this: “What platform-specific challenges does this not solve?” Their answer will tell you everything you need to know. Remember: the goal isn’t to write code once – it’s to deliver great user experiences efficiently. Sometimes that means embracing the beautiful complexity of our multi-platform world rather than trying to abstract it away. What’s your experience with cross-platform development? Have you found approaches that work well, or horror stories that confirm these concerns? I’d love to hear your thoughts in the comments below.