DEV Community

arasosman
arasosman

Posted on • Originally published at Medium

From Zero to Billions: A Deep Dive into Building a Real-World Microservice Architecture with Laravel Part:1

Image description

Part 1: The "Why" - An Introduction to Our Microservice Journey

Series: Building a High-Traffic Microservice Architecture with Laravel

Every complex software system begins with a simple goal. For us at Narbulut, the goal was ambitious yet clear: to build a file backup and storage system akin to giants like Google Drive or Dropbox, but with the flexibility and control of being self-hosted. We weren't just building a product; we were engineering a platform meant for resilience, scale, and long-term evolution.

This blog series is the story of that system. It's a deep dive into the architectural decisions, technological choices, and the hard-won lessons learned from handling over three billion requests per month. We'll explore our entire stack, from the frontend to the deepest corners of our backend infrastructure.

But before we dissect the "how," we must first understand the "why."

The Limits of a Monolith

Like many projects, the initial temptation is to build a monolithic application. A monolith is an all-in-one system where every component—user authentication, file uploads, billing, notifications—is part of a single, tightly-coupled codebase. For our Laravel-centric team, this would mean one giant Laravel application.

The initial advantages of a monolith are seductive:

  • Simplicity: One codebase, one repository, one deployment pipeline. It's straightforward to get started.
  • IDE-Friendly: Refactoring and finding code is easy when everything is in one place.
  • Unified Data: A single database holds all the application's data, making queries and transactions simple.

However, we were building a system where different parts had vastly different requirements. Consider the core tasks of our platform:

  1. API Layer: A fast, responsive interface for our frontend (a Laravel + Vue SPA) and third-party clients.
  2. File Transfers: The heavy lifting. Moving potentially huge files, a process that is network-intensive and requires robust error handling.
  3. Metadata Processing: Indexing file contents for search using tools like Elasticsearch.
  4. User Management & Authentication: A critical, security-sensitive component.

As we projected our growth, the cracks in the monolithic approach began to show:

  • Scalability Bottlenecks: If file transfers consumed massive amounts of CPU and memory, we'd have to scale the entire application—including the lightweight API endpoints—just to handle the load. This is inefficient and expensive.
  • Technology Constraints: A monolithic Laravel application would force us to solve every problem with PHP. While PHP is excellent for web APIs, is it the absolute best tool for high-throughput, long-running file transfer operations? We chose to use .NET for these heavy-duty tasks, a decision a monolith would make difficult, if not impossible.
  • Fragility: A bug in a non-critical component, like a notification service, could potentially bring down the entire platform. A memory leak in one module becomes a threat to all modules.
  • Slowing Development: As the single codebase grows, compile times get longer, test suites take forever to run, and developer onboarding becomes a nightmare. Understanding the intricate connections within a massive application is a huge cognitive load.

The Shift to a Microservice Mindset

This is why we chose the path of microservices. Instead of one big application, we decided to build a suite of small, independent services, each with a single, clear responsibility.

Our system is composed of around 10 distinct microservices. Each service is:

  • Independently Deployable: We can update, test, and deploy the user authentication service without touching the file processing service.
  • Built with the Best Tool for the Job: We use Laravel for our primary API layer, where its elegance and speed (supercharged with Octane) shine. For the intense, low-level file transfer work, we use .NET.
  • Resilient: The failure of one service (e.g., the Elasticsearch indexing service) might degrade the overall functionality (search might be temporarily unavailable), but it won't crash the entire system. Users can still upload and download their files.
  • Managed by Focused Teams: Different teams can own different services, allowing for greater autonomy and expertise.

This approach transforms our platform from a rigid, interconnected block into a flexible, living ecosystem of services that communicate with each other over well-defined APIs (both synchronous HTTP and asynchronous messages via RabbitMQ).

This decision was the foundation upon which our entire infrastructure was built. It allowed us to choose specialized tools like PostgreSQL, Redis, and Elasticsearch for the specific problems they solve best, and to orchestrate it all within a self-hosted Kubernetes environment for ultimate control and scalability.


In the next part of this series, we'll zoom out and look at the High-Level Architectural Overview, tracing the path of a request as it flows through our system, from the user's browser to the specific microservice that handles it. Stay tuned!

Part 2: The Big Picture - A High-Level Architectural Overview

Series: Building a High-Traffic Microservice Architecture with Laravel

In the first part of our series, we explored the "why"—our reasons for choosing a microservice architecture over a traditional monolith. Now, let's zoom out to the 30,000-foot view and see how all the pieces fit together. Understanding the flow of a request is key to grasping the entire system's design.

Our platform, which handles over three billion requests monthly, is a carefully orchestrated dance between various specialized services. At its core, the architecture is designed for clarity, separation of concerns, and scalability.

The Journey of a Single API Request

Imagine a user logs into our web application. They see their files, click on a folder, and the contents are displayed. This seemingly simple action triggers a sequence of events that travels across our distributed system. Let's follow that request.

graph TD
    A[User on Frontend (Laravel + Vue.js)] -->|1. HTTPS Request| B(Ocelot API Gateway);
    B -->|2. Authenticate & Route| C{Auth Service};
    C -->|JWT Valid| B;
    B -->|3. Proxy to Downstream Service| D[File API Service (Laravel Octane)];
    D -->|4. Fetch Data| E[PostgreSQL Database];
    D -->|5. Check Cache| F[Redis Cache];
    D -->|6. Return Data| B;
    B -->|7. Return Response| A;

    subgraph Kubernetes Cluster
        B;
        C;
        D;
        E;
        F;
    end
Enter fullscreen mode Exit fullscreen mode
  1. The Client: The journey starts at the user's browser, with our frontend built as a Single Page Application (SPA) using Vue.js on top of a standard Laravel installation. When the user clicks to open a folder, the Vue app makes an asynchronous API call, for instance, GET /api/v1/files?folder_id=123.

  2. The Front Door: Ocelot API Gateway: This request doesn't go directly to the service responsible for handling files. Instead, it hits our single, unified entry point: the Ocelot API Gateway. The gateway is the public face of our backend. Its primary jobs are:

    • Authentication: It inspects the request for a JWT (JSON Web Token). It might quickly validate the token itself or, for more complex checks, call our dedicated Auth Service to ensure the user is who they say they are and has the permission to access the requested data.
    • Routing: Once authenticated, the gateway consults its configuration to determine which microservice should handle this request. A request to /api/v1/files is mapped internally to the File API Service.
    • Proxying: The gateway then forwards, or "proxies," the request to the appropriate service's internal address within our Kubernetes cluster.
  3. The Workhorse: Laravel API Services: The request now arrives at one of our core backend services. Many of our primary API services are built with Laravel, running on Octane (with Swoole). Using Octane is a critical performance choice. It boots the Laravel framework once and keeps it in memory, allowing it to handle thousands of requests per second with minimal latency—perfect for a high-traffic API.

  4. Data Storage & Retrieval: The Laravel service now executes its business logic. It might:

    • Query our primary PostgreSQL database for the folder's metadata.
    • Check a Redis cache to see if this result was recently fetched, returning the cached data instantly to avoid a database hit.
    • If the request involved a search query (e.g., GET /api/v1/search?term=report), it would call our Elasticsearch service, which is optimized for complex text-based searches.
  5. The Response Journey: The Laravel service constructs a JSON response and sends it back to the Ocelot API Gateway. The gateway, in turn, sends it back to the user's browser. The Vue.js application receives the JSON data and renders the list of files on the screen.

The Supporting Cast: Infrastructure and Communication

What enables this smooth flow is the underlying infrastructure and the communication patterns we've established.

  • Kubernetes (Self-Hosted): All of our ~10 microservices live and run inside a Kubernetes cluster. Kubernetes is the orchestrator that manages deploying, scaling, and networking our services. If the File API Service becomes a bottleneck, Kubernetes can automatically scale it up from 3 instances to 10 to handle the load. We use Rancher and Lens as powerful UIs to manage and visualize our cluster.

  • Asynchronous Communication (RabbitMQ): Not all tasks happen in real-time. What if the user uploads a 10 GB video file? We can't make the user wait. In this case, the API service accepts the upload and then dispatches a ProcessVideo job to a RabbitMQ queue. A separate, dedicated .NET Worker Service, built for heavy lifting, picks up this job from the queue and handles the processing (transcoding, generating thumbnails) in the background. This decouples the initial, fast request from the long-running, resource-intensive task.

  • Observability (Graylog & Sentry): With so many moving parts, how do we know what's going on? Every service sends its logs to a centralized Graylog server. We create separate streams in Graylog for each service, so we can easily filter and analyze logs. For error and performance tracking, we use Sentry, which gives us real-time alerts and performance metrics (APM) across our entire distributed system.

This high-level view shows a system of specialized components, each chosen for a specific purpose, working in concert. It's a far cry from a single, monolithic application, but this separation is precisely what gives us the power to scale, evolve, and maintain such a high-traffic platform effectively.


Next up, we'll zoom in on the first and most critical component in this chain: The API Gateway. We'll explore in detail why it's more than just a simple proxy and how it acts as the guardian of our backend services.

Top comments (0)