Code by

Carter

Phan

LifeHub — Microservice System

LifeHub — Microservice System

Year2025
NestJSKafkaRabbitMQPostgreSQLRedisDockerPrismaTypeScript

Project Description

A system-driven project exploring distributed architecture constraints, featuring event-driven communication, idempotent processing, and failure resilience.

If you look at most coding tutorials, you'd think building software is easy. APIs always respond. Databases are always online. Requests execute exactly once. Everything is synchronous, predictable, and clean.

Then you deploy to production, and reality hits you like a freight train.

Events arrive completely out of order. Network requests time out. A service crashes halfway through processing a payment. Duplicate webhooks randomly fire across your system.

I built LifeHub precisely because I was tired of "CRUD apps" that pretend these problems don't exist. LifeHub isn't a feature-driven product; it is a system-driven experiment. I wanted to build a true, hardcore microservice architecture that strictly adheres to the hardest constraints of distributed systems: everything can (and will) fail, so the system must heal itself without human intervention.

A striking architectural visual showing a chaotic web of breaking connections slowly settling into an organized, glowing event-driven grid.

The High-Stakes Architecture

When designing LifeHub, I set several non-negotiable rules for myself to simulate real-world enterprise constraints:

  • Every service must be independently deployable and own its own database. No shared schemas allowed.
  • Services are never allowed to communicate by directly calling each other synchronously (no service chaining).
  • Everything must be fully event-driven to support partial failures gracefully.

To achieve this, I built a hybrid platform wrapping synchronous APIs for client interactions around a hardcore asynchronous backend.

The API layer is incredibly thin. Its only job is to validate intent, authenticate the user, and immediately emit a domain event. It does almost no heavy lifting. Once that event is fired, the API responds to the client instantly, and the backend machines take over.

A clean architecture diagram showing the API layer firing events into a Kafka bus alongside independent microservices.

The Dual Messaging Backbones

To handle the immense pressure of distributed messaging safely, I used a dual-messaging backbone strategy. You can't just throw everything into one queue and hope for the best.

1. Kafka for Domain Events: I used Kafka as the immutable source of truth for high-throughput business events (e.g., UserActionCreated). Because Kafka is highly scalable and allows multiple consumer groups, different microservices can react to the exact same event at their own pace without bottlenecking each other.

2. RabbitMQ for Tasks & Retries: While Kafka is great for facts, it's terrible for delayed jobs or complex retry logic. I leveraged RabbitMQ specifically to handle background tasks, exponential backoff retries, and dead-letter queues. If an email fails to send, RabbitMQ holds it and tries again exactly 5 minutes later without clogging up the main Kafka stream.

Unlearning Synchronous Thinking

The hardest part of this project wasn't setting up Docker or configuring RabbitMQ. It was completely rewiring my brain to accept eventual consistency.

When building monolithic apps, you’re used to doing everything in one massive database transaction. If step 3 fails, steps 1 and 2 automatically roll back. In LifeHub, there are no distributed transactions. If a workflow involves three different services, they update their states independently over time.

If something fails late in a workflow, you can't just "roll back" the previous service's database. I had to design complex compensating actions (essentially system "undo" commands) that automatically trigger when a failure event is broadcast.

Embracing the Duplicate: The Power of Idempotency

In any distributed system, you are mathematically guaranteed to receive duplicate messages eventually.

Rather than fruitlessly trying to prevent them, I designed every single consumer in LifeHub to be fiercely idempotent. Every event is tagged with a globally unique ID. Before a service processes anything, it checks its local database to see if it has already securely processed that exact ID. If it has, it safely ignores the duplicate and exits.

That one design choice—making duplicates completely harmless—enabled me to freely use "at-least-once" delivery guarantees natively without terrorizing my databases.

Designing for Complete Failure

Silent failures are the absolute worst enemy of a distributed system. I actively designed LifeHub to fail loudly and safely.

  • Strict Timeouts & Circuit Breakers: If a third-party dependency goes down, LifeHub enforces strict timeouts. If it keeps failing, the circuit breaker trips, isolating the dead service and protecting the rest of the system from a cascading meltdown.
  • Back-pressure: If an event storm hits, queue depth is heavily monitored. Consumers are intentionally throttled so they don't drown in requests. The system purposefully slows down, but it refuses to crash.

Debugging a system like this requires absolute visibility, not guesswork. Instead of wondering "Why did this web request fail?", I implemented heavy correlation IDs tracking events across services. You simply paste the ID into the logs and follow the trail across services to see exactly where the background job died downstream.

What LifeHub Taught Me

Most developers mistake microservices as a magical tool for scaling servers. I learned that microservices are really about scaling boundaries and fault domains.

Event-driven systems trade simplicity for extreme resilience. You pay a massive tax in upfront complexity, but what you buy is a platform that can survive massive traffic spikes, network partitions, and database outages gracefully.

Ultimately, LifeHub represents my entire philosophy on software engineering: Design systems that assume failure is inevitable, and ensure they keep working anyway.