Microservices Architecture

Building microservices with Rust for scalable distributed systems.

Advancedā±ļø 55 minšŸ“š Prerequisites: 1

Microservices Architecture

Microservices break applications into small, independent services that communicate over networks.

Microservices vs Monolith

Monolith

Single Application
ā”œā”€ā”€ All features
ā”œā”€ā”€ Shared database
└── Single deployment

Microservices

Service 1 (User Service)
ā”œā”€ā”€ Own database
└── Independent deployment

Service 2 (Payment Service)
ā”œā”€ā”€ Own database
└── Independent deployment

Service 3 (Notification Service)
ā”œā”€ā”€ Own database
└── Independent deployment

Rust Microservices Structure

Workspace Organization

workspace/
ā”œā”€ā”€ Cargo.toml              # Workspace manifest
ā”œā”€ā”€ services/
│   ā”œā”€ā”€ user-service/       # User microservice
│   │   ā”œā”€ā”€ Cargo.toml
│   │   └── src/
│   ā”œā”€ā”€ payment-service/    # Payment microservice
│   │   ā”œā”€ā”€ Cargo.toml
│   │   └── src/
│   └── notification-service/
│       ā”œā”€ā”€ Cargo.toml
│       └── src/
ā”œā”€ā”€ shared/                 # Shared code
│   ā”œā”€ā”€ common/             # Common utilities
│   │   ā”œā”€ā”€ Cargo.toml
│   │   └── src/
│   └── proto/              # gRPC definitions
│       ā”œā”€ā”€ Cargo.toml
│       └── src/
└── gateway/                # API Gateway
    ā”œā”€ā”€ Cargo.toml
    └── src/

Service Communication

1. REST API (HTTP)

RUST
// services/user-service/src/api.rs
use axum::{Router, routing::get, Json};

pub fn create_router() -> Router {
    Router::new()
        .route("/users/:id", get(get_user))
}

async fn get_user(Path(id): Path<u64>) -> Json<User> {
    // Return user data
}

// In payment-service, call user-service
use reqwest::Client;

async fn verify_user(user_id: u64) -> Result<User, Error> {
    let client = Client::new();
    let response = client
        .get(format!("http://user-service:3000/users/{}", user_id))
        .send()
        .await?;
    response.json().await
}

2. gRPC (Recommended for Inter-Service)

TOML
# Cargo.toml
[dependencies]
tonic = "0.10"
prost = "0.12"
PROTOBUF
// proto/user.proto
syntax = "proto3";

service UserService {
    rpc GetUser(GetUserRequest) returns (User);
    rpc CreateUser(CreateUserRequest) returns (User);
}

message User {
    uint64 id = 1;
    string email = 2;
}
RUST
// services/user-service/src/grpc.rs
use tonic::{Request, Response, Status};

pub struct UserServiceImpl;

#[tonic::async_trait]
impl UserService for UserServiceImpl {
    async fn get_user(
        &self,
        request: Request<GetUserRequest>
    ) -> Result<Response<User>, Status> {
        let user = get_user_from_db(request.into_inner().id).await?;
        Ok(Response::new(user))
    }
}

3. Message Queue (Async Communication)

TOML
[dependencies]
lapin = "2.0"  # RabbitMQ client
redis = "0.24"  # Redis client
RUST
// Publisher (payment-service)
use lapin::{Connection, Channel, options::*};

async fn publish_payment_event(event: PaymentEvent) -> Result<(), Error> {
    let conn = Connection::connect("amqp://localhost:5672", ConnectionProperties::default()).await?;
    let channel = conn.create_channel().await?;
    
    channel.basic_publish(
        "payments",
        "payment.completed",
        BasicPublishOptions::default(),
        &serde_json::to_vec(&event)?,
        BasicProperties::default()
    ).await?;
    
    Ok(())
}

// Consumer (notification-service)
async fn consume_payment_events() -> Result<(), Error> {
    let conn = Connection::connect("amqp://localhost:5672", ConnectionProperties::default()).await?;
    let channel = conn.create_channel().await?;
    
    let mut consumer = channel.basic_consume(
        "payment.completed",
        "notification-consumer",
        BasicConsumeOptions::default(),
        FieldTable::default()
    ).await?;
    
    while let Some(delivery) = consumer.next().await {
        let event: PaymentEvent = serde_json::from_slice(&delivery.data)?;
        send_notification(event).await?;
    }
    
    Ok(())
}

API Gateway

RUST
// gateway/src/main.rs
use axum::{Router, routing::get, extract::Path};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/api/users/*path", proxy_to_user_service)
        .route("/api/payments/*path", proxy_to_payment_service);
    
    axum::serve(listener, app).await.unwrap();
}

async fn proxy_to_user_service(Path(path): Path<String>) -> Response {
    // Forward request to user-service
    // Add authentication, rate limiting, etc.
}

Service Discovery

RUST
// shared/service-discovery/src/lib.rs
use std::collections::HashMap;

pub struct ServiceRegistry {
    services: HashMap<String, String>,
}

impl ServiceRegistry {
    pub fn get_service_url(&self, service_name: &str) -> Option<&String> {
        self.services.get(service_name)
    }
    
    pub fn register_service(&mut self, name: String, url: String) {
        self.services.insert(name, url);
    }
}

Configuration Management

RUST
// Each service has its own config
// config/user-service.toml
[database]
url = "postgres://localhost/user_db"

[server]
port = 3001

[services]
payment_service_url = "http://payment-service:3002"

Docker Compose for Development

YAML
version: '3.8'
services:
  user-service:
    build: ./services/user-service
    ports:
      - "3001:3001"
    environment:
      - DATABASE_URL=postgres://db:5432/user_db
  
  payment-service:
    build: ./services/payment-service
    ports:
      - "3002:3002"
    environment:
      - DATABASE_URL=postgres://db:5432/payment_db
  
  api-gateway:
    build: ./gateway
    ports:
      - "3000:3000"
    depends_on:
      - user-service
      - payment-service
  
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"

Best Practices

  • Database per service: Each service has its own database
  • API versioning: Version your APIs
  • Circuit breakers: Handle service failures gracefully
  • Distributed tracing: Track requests across services
  • Health checks: Monitor service health
  • Configuration externalization: Use environment variables
  • Stateless services: Don't store session state
  • Idempotency: Make operations safe to retry

Code Examples

Microservice Structure

Organizing microservices in workspace

RUST
fn main() {
    println!("Microservices architecture:");
    println!("- Each service is independent");
    println!("- Own database per service");
    println!("- Communicate via HTTP/gRPC");
    println!("- Deploy independently");
}

Explanation:

Microservices are organized as separate crates in a workspace. Each service is independently deployable and has its own database.

Service Communication

Services communicating via HTTP

RUST
struct PaymentService {
    user_service_url: String,
    client: reqwest::Client,
}
impl PaymentService {
    async fn verify_user(&self, user_id: u64) -> Result<User, Error> {
        Ok(User { id: user_id, name: String::from("User") })
    }
}
struct User {
    id: u64,
    name: String,
}
fn main() {
    println!("Service communication:");
    println!("- HTTP REST: Simple, widely supported");
    println!("- gRPC: Efficient, type-safe");
    println!("- Message Queue: Async, decoupled");
}

Explanation:

Microservices communicate via HTTP REST, gRPC, or message queues. Each method has trade-offs: REST is simple, gRPC is efficient, queues are async.

Exercises

Service Structure

Create a workspace with two services!

Medium

Starter Code:

RUST
fn main() {
    println!("Workspace created!");
}