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
YAMLversion: '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!
Starter Code:
RUST
fn main() {
println!("Workspace created!");
}