11 years. 4 tech stacks. 1 relentless obsession with making things better. This is the story of how a Christmas break project — a simple offline lottery number generator — evolved into a cloud-native lotto results platform built in Rust and deployed globally on serverless infrastructure.
Why Build a Lottery App?
In 2015, the Philippine lottery scene was booming. People checked results obsessively, tracked number patterns, and debated "lucky" combinations. The existing apps were clunky, ad-heavy, and required internet for the simplest feature: generating random numbers.
The idea was dead simple: open the app, tap a button, get a combination. No account, no internet, no friction. That was Linotto v1 — a Christmas break project born from a personal itch during the holiday downtime. It shipped to both the Play Store and App Store within days.
What I didn't anticipate was that it would become my longest-running engineering playground — a project that forced me to confront every hard decision in software engineering over the next decade, eventually growing into a full lotto results platform serving draw results, jackpot alerts, and ticket tracking to users across the Philippines, peaking at ~60,000 daily active users.
Phase 1: The Native Era (2015–2016)
The Approach
Back then, "cross-platform" meant writing the same app twice. I built Linotto natively — Java for Android, Swift for iOS. Two codebases, two build pipelines, two sets of bugs to chase. Every feature was implemented, tested, and shipped in parallel. The initial version came together over Christmas break 2015 — a focused burst of coding during the holiday downtime.
Why Native?
- Performance. Native apps had zero overhead — critical for an app that needed to feel instant.
- Platform fidelity. Material Design on Android, UIKit on iOS. Users expected native look and feel.
- Maturity. In 2015, cross-platform options (Cordova, Xamarin, early React Native) were either too slow, too buggy, or too limiting for production use.
Going Cloud-Native (Early 2016)
Users quickly outgrew the offline generator. They wanted actual PCSO draw results, jackpot notifications, and ticket tracking. The app needed to become a proper lotto results platform. So I built a backend: a monolithic Java Spring Boot application running on an EC2 instance. It scraped PCSO results, stored them in MongoDB, and pushed notifications through Firebase.
Trade-offs
- Double development effort. Every feature took twice the time. A simple UI change meant implementing it in Java, then reimplementing it in Swift — with different layout systems, different state management patterns, and different debugging tools.
- EC2 cost inefficiency. The server ran 24/7 regardless of traffic. Draw results were only generated three times a day, but the instance didn't care about your traffic patterns.
- Operational burden. SSH in, pull the latest code, restart the service. "DevOps" was a bash script and a prayer that nothing broke overnight.
Phase 2: Going Serverless (Late 2016–2023)
The Rationale
The EC2 instance had only been running for a few months, but the inefficiency was already obvious. Monthly bills didn't correlate with usage. As a lotto results platform, traffic spiked only during draw times (three draws a day) and flatlined between them. I was paying for 168 hours of compute to serve bursts of traffic around each draw.
AWS Lambda changed the math completely. Pay-per-invocation meant the infrastructure cost finally matched the traffic pattern: scale to zero when idle, scale up instantly when users checked their numbers after a draw.
The Migration
The monolith became microservices — separate Lambda functions for authentication, results scraping, user management, and push notifications. API Gateway handled routing. MongoDB Atlas replaced the self-managed database.
I chose Python for the rewrite because it offered the fastest path from Java Spring Boot to Lambda. The AWS SDK (boto3) was first-class in Python, the deployment packages were straightforward, and the development cycle was significantly faster than compiled Java.
Benefits
- Cost reduction of 80%+. From a fixed ~$30–50/month EC2 instance to pennies in Lambda invocations. The generous free tier covered most of the compute.
- Zero scaling concerns. Lambda handles concurrency automatically. No capacity planning, no auto-scaling groups, no load balancers to configure.
- No patching. No operating system to maintain. No security patches at midnight. AWS manages the runtime.
- Faster deployments. Update a single function in seconds instead of redeploying an entire application.
Trade-offs
- Cold starts. Python Lambda functions had 200–300ms cold starts. Noticeable on the first request after idle periods, though acceptable for a mobile app where users expected a brief loading state.
- Vendor lock-in. The architecture became deeply coupled to AWS-specific services (API Gateway, Lambda, SES, SNS). Migrating away would mean rewriting the orchestration layer entirely.
- Debugging complexity. Distributed microservices are harder to debug than a monolith. A request might touch 3–4 services before returning a response.
- Package size limits. Python dependencies (especially those with native binaries) had to be carefully managed to stay within Lambda's deployment package limits.
Phase 3: The Flutter Migration (2023–2025)
The Breaking Point
After 7+ years of maintaining two native codebases, the overhead became unsustainable for a solo developer. Every new feature took twice the effort. Bug fixes had to be verified on two platforms with completely different toolchains. The cognitive load of context-switching between Java/Kotlin and Swift/UIKit for the same logical feature was draining.
Why Flutter?
- Single codebase, dual deployment. One Dart codebase compiling to native ARM code for both iOS and Android. Not a webview. Not interpreted JavaScript. Actual compiled binaries with near-native performance.
- Proven maturity. By 2023, Flutter had been battle-tested by Google (Google Pay, Stadia), Alibaba, BMW, and countless production apps. The early-adopter risk was gone.
- Hot reload. Sub-second UI iteration. The development feedback loop collapsed from minutes (compile → deploy → navigate to screen) to milliseconds.
- Widget-based architecture. Composable, testable UI components that work identically on both platforms. No more "it looks right on Android but breaks on iOS" surprises.
- Strong ecosystem. Mature packages for state management (Riverpod), HTTP (Dio), local storage, and push notifications — all the building blocks a mobile app needs.
Benefits
- Development velocity doubled. Features that previously took a week (implement twice, test twice) now took 2–3 days.
- UI consistency. Pixel-perfect matching across platforms without platform-specific overrides.
- Single debugging workflow. One debugger, one set of breakpoints, one mental model for the entire app.
- Easier onboarding. If I ever brought on a contributor, they'd only need to learn one framework instead of two.
Trade-offs
- Larger binary size. Flutter apps ship with the Skia rendering engine, resulting in larger APKs/IPAs compared to lean native apps. For Linotto, this was ~15–20 MB overhead — acceptable given modern device storage.
- Platform-specific features require plugins. Deep OS integrations (widgets, certain notification behaviors, background processing) still need platform channels or community plugins that may lag behind native SDK updates.
- Full rewrite cost. Migrating from two mature native apps to Flutter meant rebuilding everything from scratch — navigation, state management, API integration, local caching, push notifications. This was a multi-month effort.
Phase 4: The Rust Rewrite & Vibe-Coding (2026)
Why Rust?
Performance mattered. Every millisecond of cold start was a user staring at a loading spinner. Python's cold starts (200–300ms) and warm latency (50–100ms) were acceptable in 2016, but by 2026 users expected instant responses from mobile APIs.
Rust offered a unique combination for Lambda workloads:
- Near-zero cold starts. Rust compiles to a single static binary with no runtime, no garbage collector, no interpreter to initialize. Cold starts dropped from 200–300ms to 50–150ms.
- Minimal memory footprint. No runtime overhead means Lambda functions run comfortably in 64–128 MB of memory, down from 256 MB for Python. At scale, this directly translates to lower costs.
- Zero-cost abstractions. High-level patterns (iterators, generics, async/await) compile down to the same machine code you'd write by hand. You don't pay for expressiveness.
- Memory safety without garbage collection. The borrow checker catches entire classes of bugs (null pointer dereferences, data races, use-after-free) at compile time. If it compiles, it's memory-safe.
Performance Results
| Metric | Python | Rust | Improvement |
|---|---|---|---|
| Cold start | 200–300ms | 50–150ms | ~2–3x faster |
| Warm latency | 50–100ms | 5–15ms | ~5–10x faster |
| Memory usage | 256 MB | 64–128 MB | ~2–4x less |
| Binary size | 10–20 MB (with deps) | 5–10 MB | ~2x smaller |
Optimization Techniques
- ARM64 (Graviton2). AWS Graviton processors offer 20% better price-performance than x86. Rust's cross-compilation story makes targeting aarch64 trivial.
- Aggressive binary optimization.
opt-level = "z"for size optimization, LTO (Link-Time Optimization) for cross-crate inlining, stripped debug symbols for minimal deployment artifacts. - Connection pooling for Lambda. Reusing database connections across warm invocations eliminates the overhead of establishing new connections on every request.
- JWT validation with RS256. Asymmetric signing means Lambdas only need the public key to validate tokens — no shared secrets, no network calls to an auth server.
- Argon2id password hashing. Memory-hard hashing that's resistant to GPU-based brute force attacks, running efficiently on Lambda's limited memory allocation.
Trade-offs
- Steeper learning curve. Rust's ownership system, lifetimes, and borrow checker require a mental model shift. The compiler is strict — it rejects code that other languages would silently accept (and crash at runtime).
- Slower iteration speed. Compile times for Rust are longer than Python's zero-compile workflow. A full build can take 30–60 seconds. Incremental builds are faster (5–10s), but still slower than interpreted languages.
- Smaller ecosystem for web/serverless. While growing rapidly, Rust's ecosystem for AWS Lambda (aws-lambda-rust-runtime) and web frameworks (Axum, Actix) is younger than Python's battle-tested boto3 and Flask/FastAPI.
- Recruitment challenge. If Linotto ever needed contributors, finding Rust developers is harder than finding Python or TypeScript developers. The talent pool is smaller.
Vibe-Coding: AI-Assisted Development
The Rust rewrite coincided with AI-assisted development becoming deeply embedded in my workflow. The combination was transformative — Rust's strict compiler caught the mistakes that AI-generated code might introduce, while the AI accelerated the boilerplate-heavy parts of the migration (serialization structs, error types, API endpoint scaffolding).
Benefits
- Collapsed research time. Instead of spending hours reading documentation for a crate I'd use once, I described what I needed and iterated on the output.
- Faster boilerplate generation. Rust is expressive but verbose for CRUD operations. AI handled the repetitive scaffolding — struct definitions, trait implementations, error conversions — in minutes instead of hours.
- Architectural sounding board. The AI challenged design decisions, suggested patterns I hadn't considered, and helped me evaluate trade-offs from multiple angles.
- Documentation as a byproduct. The conversational flow naturally produced architecture docs, API specs, and deployment guides as part of the development process.
Trade-offs
- Verification overhead. AI-generated code requires careful review. Subtle bugs, incorrect assumptions, and hallucinated APIs can slip through if you're not actively validating the output.
- Over-reliance risk. It's tempting to accept generated code without fully understanding it. This creates knowledge gaps that surface during debugging or when the AI isn't available.
- Context limitations. AI assistants have finite context windows. Large codebases require careful context management — you can't just dump an entire project and expect coherent suggestions.
The Current Architecture
Mobile
Flutter — single codebase targeting iOS and Android. State management with Riverpod. Local caching for offline access to recent results.
Backend
Rust on AWS Lambda (ARM64 Graviton2). Separate functions for auth, results, user management, and notifications. API Gateway for routing and throttling.
Database
MongoDB Atlas — managed, multi-AZ, auto-scaling. Chosen early for its flexible schema (lottery game rules vary widely) and maintained for continuity.
Infrastructure
API Gateway, Lambda, SES (email), S3 (assets), CloudFront (CDN), SNS (push notifications). All serverless, all pay-per-use.
Lessons Learned Over 11 Years
1. Don't Be Afraid to Rewrite
Every major rewrite delivered compounding returns:
- Java/Swift → Flutter: halved development time, unified the codebase
- EC2 → Lambda: cut costs by 80%+, eliminated ops burden
- Python → Rust: 5–10x faster responses, 2–4x less memory
The key insight: rewrites are expensive upfront but pay dividends for years. The trick is timing — rewrite when the pain of the current system exceeds the cost of migration, not when a new technology is merely shiny.
2. Optimize for Iteration Speed
Cross-platform frameworks, serverless infrastructure, and AI-assisted development all share the same goal: spend less time on plumbing, more time on the product. Every technology choice should be evaluated through the lens of "how fast can I ship a feature from idea to production?"
3. Embrace Tools When They're Ready, Not When They're Hyped
- I adopted Flutter in 2023, not 2018 when it launched — because it needed time to mature.
- I went serverless in 2016 when Lambda pricing made economic sense for my traffic pattern.
- I picked Rust in 2026 when cold start performance became the bottleneck, not when Rust first appeared on HN.
- I embraced AI-assisted development when it genuinely accelerated output, not when GPT-3 first launched.
Timing matters more than trends. Let others absorb the early-adopter pain.
4. Side Projects Deserve Engineering Rigor
Linotto has no VC funding, no team of engineers, no SLAs to meet. But treating it with the same rigor as a funded product forced me to develop skills that transferred directly to my day job: infrastructure design, cost optimization, CI/CD, observability, and architectural decision-making under real constraints.
5. Start Simple, Evolve Relentlessly
The first version was a Christmas break project. The current version is a cloud-native lotto results platform. The distance between those two points wasn't covered in a single leap — it was hundreds of small decisions, each one making the system slightly better than before.
The Timeline
| Year | Milestone |
|---|---|
| 2015 | Offline number generator launched (Android Java + iOS Swift) |
| Early 2016 | Backend goes cloud-native (Java Spring Boot on EC2, MongoDB, Firebase) |
| Late 2016 | Backend goes serverless (EC2 → AWS Lambda Python) |
| 2023 | Mobile apps unified under Flutter (single codebase, dual deployment) |
| 2026 | Backend rewritten in Rust with AI-assisted vibe-coding |
TL;DR
A Christmas break lottery number generator, built in 2015, evolved through 4 tech stack migrations into a cloud-native lotto results platform with a Rust serverless backend and Flutter mobile apps — all maintained by one person. Each migration was driven by a specific pain point (cost, performance, developer velocity), not hype. The result: a faster, cheaper, and more maintainable system at every stage.
Check out linotto.com to see where the project stands today.
Side projects are the best investment you can make in your engineering career. They give you the freedom to experiment, fail, rewrite, and ultimately build something that reflects a decade of accumulated judgment. What started as a Christmas break number picker is now a lotto results platform serving real users — and it's still evolving. Here's to the next decade. 🚀