← Portfolio
← SELECTED WORK
ENGINEERING CASE STUDY / 02

A city to explore.
A system to trust.

Popin / Map-first event discovery

I built the mobile apps, backend, and organizer dashboard—and the testing, delivery, and observability systems needed to keep them moving together.

3connected product surfaces
5dedicated background queues
2engineering interns mentored
MY ROLE Lead developer & system architect · Flutter · Backend · Delivery automation
01 / THE PRODUCT & THE PROBLEM

“What’s happening
right around me?”

A map makes that question intuitive. Behind it, every pan and zoom can trigger a network request, a database query, and another expensive marker render. I worked across the client and backend to keep that interaction lightweight.

discovery / viewport-query
Illustrative event viewportThe viewport automatically pans between neighborhoods. Only events inside its bounds are highlighted.WEST QUARTEREAST QUARTER
Illustration · synthetic events · no live location data
QUERY WHAT THE USER CAN SEE

A moving map.
A bounded request.

I tied discovery to the visible map bounds, debounced movement, and reused marker bitmaps. Each layer avoids work the user doesn’t need.

01 Wait for movement to settle

02 Query the visible bounds

03 Reuse cached marker images

CLIENT / REQUEST LESS

Wait for the map to settle.

A debounced viewport listener reduces request bursts. Bounding-box queries limit discovery to the visible area, while coordinate and status indexes support filtering.

SERVER / SHARE THE WORK

Coalesce identical requests.

In-flight Promise deduplication lets identical lookups share one pending result. Redis TTL caching reuses results across requests instead of repeatedly rebuilding them.

RENDERING / REUSE PIXELS

Rasterize a marker once.

Custom Flutter widgets become map-marker bitmaps. An in-memory bitmap cache avoids repeating that CPU-intensive conversion on each render cycle.

THE BOUNDARY MATTERS

An in-flight Promise is shared within a process; it is not a distributed lock across every server replica. Cache lifetimes balance freshness and query load, and coordinate indexes still need validation against the actual query plan.

02 / CONNECTED SYSTEMS

Three surfaces.
One coordinated backend.

Flutter discovery, organizer workflows, and real-time conversations share API contracts, authentication, and persisted state. Heavy work moves into dedicated queues.

popin / system-architecture LOGICAL VIEW
Popin system architectureFlutter mobile and React organizer clients connect to the Express TypeScript backend via REST and Socket.IO. The backend uses PostgreSQL through Prisma, Redis for caching and queues, BullMQ workers for background tasks, and external services. OpenTelemetry and Pino capture API telemetry.Flutter mobile appReact organizer dashboardExpress + TypeScriptPostgreSQL + PrismaRedisOpenTelemetry + PinoBullMQ workersExternal servicesDiscovery · chat · attendanceEvents · teams · analyticsREST · Socket.IO · Better Auth · ZodControllers → servicesEvents · users · messagesTTL cache · job queuesPrisma spans → Grafana / TempoEmail · notifications · processingReports · maintenanceOneSignal · Resend · S3OpenAI moderationREST / authenticated socketsREAD / WRITECACHE / ENQUEUEAPI TELEMETRYCONSUMESERVICE CALLSSelected dependencies shown. External services are used by the API and relevant background workers.

One identity model

Better Auth provides organization membership and role-based access. Web sessions use HTTP-only cookies; Flutter persists its cookie jar, with a backend Bearer fallback.

Workload-specific queues

Separate email, notification, data-processing, report, and maintenance queues have distinct concurrency limits, keeping slow work from consuming the same worker capacity.

Fast acknowledgement

Event creation saves the event and queues follow-up work. Push notifications and email delivery run asynchronously with retries instead of holding the request open.

03 / REAL-TIME COORDINATION

A conversation has
more than one state.

Sending a message is easy. Delivering it to the right room, preserving it, tracking reads, and handling unsafe content requires a coordinated workflow.

Authenticated Socket.IO connections join event or direct-message rooms. Persisted messages, read receipts, typing indicators, and offline unread updates keep the experience consistent across sessions.

The moderation path classifies content and masks flagged text before broadcast. Schema-constrained OpenAI responses support predictable handling, while adding an external dependency and latency to that path.

The engineering goal: isolate conversations, preserve state, and make the moderation decision explicit.

04 / CI/CD & DEVOPS

A commit is a promise.
The pipeline checks it.

Manual release steps and inconsistent test environments make delivery fragile. I built repeatable backend and Android pipelines that turn each change into test evidence, a versioned artifact, and a controlled release.

popin / continuous-delivery
BACKENDReal dependencies. Reviewable evidence.
ANDROIDReproducible tests. Automated distribution.
THE DESIGN DECISION

Make releases repeatable, not memorable.

Real PostgreSQL and Redis services make integration checks meaningful. A consistent Flutter image makes visual tests trustworthy. Versioned artifacts and automated delivery reduce the manual steps between a change and its release.

Illustrative flow · select a stage for its engineering rationale
BACKEND / CONFIDENCE BEFORE RELEASE

Test against the system you run.

I used Docker-in-Docker to provision PostgreSQL and Redis, apply migrations, and exercise API behavior against real persistence. Unit, integration, and configured Gatling checks provide different kinds of evidence.

BuildKit reuses unchanged layers; the resulting image is pulled and deployed through Docker Compose. Tests and repeatable packaging reduce release uncertainty.

ANDROID / LESS RELEASE FRICTION

Let the trigger choose the destination.

Feature branches produce QA APKs. Dev builds go to Play internal testing; release tags trigger production delivery through Fastlane. Main builds a production bundle without automatically publishing it.

Signing credentials are injected in CI, while version metadata and generated release notes travel with the build. This replaces a sequence of manual signing, upload, and documentation steps.

05 / A SMALL FAILURE WITH A BIG LESSON

Same UI.
Different pixels.

Flutter golden tests passed on macOS and failed on Linux CI. Font rendering, antialiasing, and missing fonts made a visual regression check report differences that were caused by the environment.

THE SOURCE OF THE MISMATCH
macOS reference
PopinDiscover nearby
Linux runner
PopinDiscover nearby

Exaggerated illustration of rendering differences, not actual test screenshots.

THE FIX

Make the environment
part of the test.

I containerized reference generation using the same Linux Flutter image as CI. Developers regenerate golden images through a Docker script, so baseline and test execution share the rendering environment.

LOCAL DOCKER=CI IMAGE

Persistent Pub and Dart-tool volumes reuse dependencies while keeping the workflow compatible with local development tools.

WHY IT MATTERS

A reliable test must distinguish a product regression from an environment difference. Matching the runtime makes golden failures more actionable; updating a baseline still requires human review of the intended UI change.

06 / ENGINEERING EVIDENCE

Don’t stop at
a green pipeline.

I connected test results, deployment metadata, runtime logs, and traces so a failure could be investigated in context.

quality / evidence-flow REPORTING ARCHITECTURE
01

Test evidence

JUnit results, coverage, integration tests, and Gatling reports converge in a GitLab Pages quality portal.

02

Release context

Commit SHA, message, and branch are injected into load-test report headers, connecting a result to the code that produced it.

03

Runtime evidence

Staging container logs become browsable HTML reports. Scheduled pipelines compile 24-hour NDJSON span reports for analysis.

Follow the request.

OpenTelemetry and Prisma instrumentation connect request handling to database work. Pino structured logs redact sensitive fields; Grafana/Tempo makes spans inspectable.

Measure a journey.

Gatling exercises a user flow rather than just an isolated endpoint. The 200-user scenario and zero-failed-request assertion are test configuration—not a claim that every real-world load will pass.

07 / OWNERSHIP BEYOND THE CODE

Build the system.
Help others build it.

I led two engineering interns across onboarding, analytics, and testing, breaking product work into tasks with clear interfaces and review expectations.

Shared API contracts and validation schemas made the boundaries explicit. Branch conventions, code reviews, and reproducible integration runners gave contributors a repeatable way to validate their work.

The same engineering principle runs through the product and the delivery tooling: make the next step understandable, observable, and repeatable.

THE ENGINEERING THROUGH-LINE

Care in the details.
Confidence in the release.

A smooth map, a responsive API, a trustworthy test, and a traceable release are parts of the same experience. Popin brought those parts together.

← Back to selected workExplore LetzGo’s route engine ↗