Introduction
Welcome back, intrepid cloud architect! In our previous projects, we’ve built full-stack applications and standalone API services, mastering the fundamentals of Void Cloud deployment and configuration. Now, it’s time to tackle a more advanced, yet incredibly powerful, architectural pattern: Microservices.
Microservices represent a shift from monolithic applications (where all functionality resides in a single, large codebase) to a collection of small, independent services. Each service focuses on a single business capability, can be developed, deployed, and scaled independently, and communicates with other services through well-defined APIs. This approach offers significant benefits in terms of scalability, resilience, and development agility, especially for large and complex applications.
In this chapter, we’ll design, implement, and deploy a simple e-commerce-like microservices architecture on Void Cloud. We’ll create separate services for users, products, and orders, along with an API Gateway to act as the single entry point for our clients. Void Cloud’s serverless functions and managed deployment capabilities make it an ideal platform for hosting such distributed systems. Get ready to build something truly robust and scalable!
Prerequisites: Before we dive in, ensure you’re comfortable with:
- Basic Void Cloud operations using the
vcCLI (deployment, environment variables, project setup). - Developing serverless functions, preferably with Node.js and TypeScript.
- Understanding of HTTP requests and API design.
Core Concepts: Understanding Microservices on Void Cloud
Building microservices is about breaking down a complex problem into smaller, manageable pieces. On Void Cloud, each of these pieces can often be a separate serverless function or a small application deployed as an independent Void Cloud project.
What is a Microservice?
Imagine a traditional restaurant where one chef handles everything: taking orders, cooking, serving, and even washing dishes. That’s a monolith. Now, imagine a modern kitchen with specialized stations: one chef for appetizers, another for main courses, a pastry chef, and a dedicated waitstaff. Each “station” is a microservice. They all contribute to the overall dining experience but operate independently.
A microservice is:
- Small and focused: Handles one specific business capability (e.g., user management, product catalog).
- Independent: Can be developed, deployed, and scaled without affecting other services.
- Loosely coupled: Communicates with other services via APIs (usually HTTP/REST or message queues).
- Technology agnostic: Can use different programming languages or databases, though we’ll stick to Node.js/TypeScript for consistency.
Why Microservices for Void Cloud?
Void Cloud’s architecture is inherently well-suited for microservices due to its:
- Serverless Functions: Each microservice can be deployed as a Void Cloud Serverless Function. This means you only pay for compute when your service is actively handling requests, and scaling is handled automatically.
- Independent Deployments: Each service lives in its own Void Cloud project (or a sub-directory within a larger monorepo, managed by Void Cloud’s build system), allowing for independent deployment pipelines.
- Managed Routing & API Gateway: Void Cloud offers robust routing capabilities. While you can build a custom API Gateway, Void Cloud’s platform-level routing can act as a powerful ingress point for your microservices.
- Environment Isolation: Services run in isolated environments, preventing issues in one service from directly impacting others.
- Secrets Management: Securely manage API keys, database credentials, and other sensitive information for each service.
Our Microservices Architecture
For this project, we’ll build a simplified e-commerce backend with the following services:
- User Service: Manages user registration, profiles, and authentication (for simplicity, we’ll just handle basic user data).
- Product Service: Manages the product catalog, including listing available products and retrieving product details.
- Order Service: Handles the creation of new orders. This service will need to interact with both the User Service (to verify the user) and the Product Service (to fetch product details and prices).
- API Gateway: A single entry point that exposes a unified API to client applications (e.g., a frontend web app or mobile app). It will route incoming requests to the appropriate backend microservice.
Here’s how these services will interact:
Explanation of the Diagram:
- The
Clientmakes requests only to theAPI Gateway. - The
API Gatewayacts as a reverse proxy, forwarding requests to the correctMicroservicebased on the URL path. - The
Order Serviceneeds information from bothUser ServiceandProduct Serviceto fulfill an order, demonstrating inter-service communication. - Each service has its own (mock) data store, emphasizing independence.
Inter-Service Communication
A critical aspect of microservices is how they talk to each other. For simplicity, we’ll use direct HTTP requests between our services. In a real-world scenario, you might also consider message queues (like RabbitMQ or Kafka) for asynchronous communication, but HTTP is a great starting point.
When deploying services on Void Cloud, each service gets its own unique URL. We’ll use Void Cloud’s environment variables to pass these URLs between services, allowing them to discover and communicate with each other.
Step-by-Step Implementation: Building Our Microservices
Let’s get our hands dirty! We’ll create a monorepo structure to keep our services organized.
Step 1: Project Setup
First, create a new directory for our entire microservices project.
mkdir void-microservices-ecommerce
cd void-microservices-ecommerce
Now, let’s create subdirectories for each of our services and the API Gateway. Each of these will be treated as an independent Void Cloud project for deployment purposes.
mkdir services
mkdir api-gateway
mkdir services/user-service
mkdir services/product-service
mkdir services/order-service
We’ll use Node.js with TypeScript for all our services. Let’s initialize package.json and a basic tsconfig.json in each service.
# For User Service
cd services/user-service
npm init -y
npm install express typescript @types/express @types/node ts-node-dev
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib es2020 --module commonjs --target es2020
mkdir src
cd ../../.. # Back to root
# Repeat for Product Service
cd services/product-service
npm init -y
npm install express typescript @types/express @types/node ts-node-dev
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib es2020 --module commonjs --target es2020
mkdir src
cd ../../.. # Back to root
# Repeat for Order Service
cd services/order-service
npm init -y
npm install express typescript @types/express @types/node ts-node-dev node-fetch @types/node-fetch # node-fetch for HTTP calls
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib es2020 --module commonjs --target es2020
mkdir src
cd ../../.. # Back to root
# Repeat for API Gateway
cd api-gateway
npm init -y
npm install express typescript @types/express @types/node ts-node-dev http-proxy-middleware @types/http-proxy-middleware
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib es2020 --module commonjs --target es2020
mkdir src
cd ../.. # Back to root
Void Cloud Project Initialization:
For each service, we’ll also run vc init. Void Cloud will detect the package.json and suggest a Node.js project. Accept the defaults.
cd services/user-service
vc init
# Follow prompts: Link to existing project? No. Project name: user-service.
cd ../../..
cd services/product-service
vc init
# Follow prompts: Link to existing project? No. Project name: product-service.
cd ../../..
cd services/order-service
vc init
# Follow prompts: Link to existing project? No. Project name: order-service.
cd ../../..
cd api-gateway
vc init
# Follow prompts: Link to existing project? No. Project name: api-gateway.
cd ../..
Now you should have a void.json file in each service directory.
Step 2: Implement the User Service
Let’s create a simple User Service.
File: services/user-service/src/index.ts
// services/user-service/src/index.ts
import express from 'express';
const app = express();
app.use(express.json()); // Enable JSON body parsing
// Mock user data store
interface User {
id: string;
name: string;
email: string;
}
const users: User[] = [
{ id: 'usr_123', name: 'Alice Smith', email: 'alice@example.com' },
{ id: 'usr_456', name: 'Bob Johnson', email: 'bob@example.com' },
];
// Route to get all users
app.get('/users', (req, res) => {
console.log('User Service: Fetching all users');
res.status(200).json(users);
});
// Route to get a user by ID
app.get('/users/:id', (req, res) => {
const { id } = req.params;
console.log(`User Service: Fetching user with ID: ${id}`);
const user = users.find(u => u.id === id);
if (user) {
res.status(200).json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
});
// Route to create a new user (mock implementation)
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ message: 'Name and email are required' });
}
const newUser: User = { id: `usr_${Date.now()}`, name, email };
users.push(newUser);
console.log('User Service: New user created', newUser);
res.status(201).json(newUser);
});
// Define the port, defaulting to 3000 for local development
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`User Service listening on port ${PORT}`);
});
Update package.json scripts:
Open services/user-service/package.json and add these scripts:
// services/user-service/package.json
{
// ... other fields
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
// ... other fields
}
Now, let’s deploy the User Service to Void Cloud.
cd services/user-service
vc deploy
Void Cloud will build and deploy your service. Once deployed, you’ll get a URL like https://user-service-xxxx.void.app. Make a note of this URL; we’ll need it later for inter-service communication.
You can test it directly:
curl https://user-service-xxxx.void.app/users
curl https://user-service-xxxx.void.app/users/usr_123
Step 3: Implement the Product Service
Next, the Product Service, following a similar structure.
File: services/product-service/src/index.ts
// services/product-service/src/index.ts
import express from 'express';
const app = express();
app.use(express.json());
// Mock product data store
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
const products: Product[] = [
{ id: 'prod_a1', name: 'Void Cloud T-Shirt', price: 25.00, stock: 100 },
{ id: 'prod_b2', name: 'Void Cloud Mug', price: 12.50, stock: 50 },
{ id: 'prod_c3', name: 'Void Cloud Sticker Pack', price: 5.00, stock: 200 },
];
// Route to get all products
app.get('/products', (req, res) => {
console.log('Product Service: Fetching all products');
res.status(200).json(products);
});
// Route to get a product by ID
app.get('/products/:id', (req, res) => {
const { id } = req.params;
console.log(`Product Service: Fetching product with ID: ${id}`);
const product = products.find(p => p.id === id);
if (product) {
res.status(200).json(product);
} else {
res.status(404).json({ message: 'Product not found' });
}
});
// Define the port
const PORT = process.env.PORT || 3001; // Use a different port for local dev
app.listen(PORT, () => {
console.log(`Product Service listening on port ${PORT}`);
});
Update package.json scripts:
Open services/product-service/package.json and add these scripts:
// services/product-service/package.json
{
// ... other fields
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
// ... other fields
}
Deploy the Product Service:
cd services/product-service
vc deploy
Again, note the deployed URL (e.g., https://product-service-yyyy.void.app).
Step 4: Implement the Order Service (with Inter-Service Communication)
This is where things get interesting! The Order Service will need to call the User and Product services.
File: services/order-service/src/index.ts
// services/order-service/src/index.ts
import express from 'express';
import fetch from 'node-fetch'; // For making HTTP requests
const app = express();
app.use(express.json());
// Mock order data store
interface Order {
id: string;
userId: string;
productId: string;
quantity: number;
totalPrice: number;
status: 'pending' | 'completed' | 'cancelled';
}
const orders: Order[] = [];
// Environment variables for service URLs
// These will be set on Void Cloud deployment
const USER_SERVICE_URL = process.env.VOID_USER_SERVICE_URL;
const PRODUCT_SERVICE_URL = process.env.VOID_PRODUCT_SERVICE_URL;
// Route to create a new order
app.post('/orders', async (req, res) => {
const { userId, productId, quantity } = req.body;
if (!userId || !productId || !quantity || quantity <= 0) {
return res.status(400).json({ message: 'Invalid order data' });
}
console.log(`Order Service: Attempting to create order for userId: ${userId}, productId: ${productId}`);
if (!USER_SERVICE_URL || !PRODUCT_SERVICE_URL) {
console.error('Order Service: Missing service URLs in environment variables!');
return res.status(500).json({ message: 'Service configuration error' });
}
try {
// 1. Validate User
const userResponse = await fetch(`${USER_SERVICE_URL}/users/${userId}`);
if (!userResponse.ok) {
console.warn(`Order Service: User not found or service error for ID: ${userId}`);
return res.status(404).json({ message: 'User not found' });
}
const user = await userResponse.json();
console.log('Order Service: User validated', user.name);
// 2. Fetch Product Details
const productResponse = await fetch(`${PRODUCT_SERVICE_URL}/products/${productId}`);
if (!productResponse.ok) {
console.warn(`Order Service: Product not found or service error for ID: ${productId}`);
return res.status(404).json({ message: 'Product not found' });
}
const product = await productResponse.json();
console.log('Order Service: Product details fetched', product.name);
if (product.stock < quantity) {
return res.status(400).json({ message: 'Not enough stock available' });
}
// 3. Create Order
const totalPrice = product.price * quantity;
const newOrder: Order = {
id: `ord_${Date.now()}`,
userId,
productId,
quantity,
totalPrice,
status: 'completed', // Simplified, no actual payment processing
};
orders.push(newOrder);
console.log('Order Service: Order created successfully', newOrder);
res.status(201).json(newOrder);
} catch (error) {
console.error('Order Service: Error creating order:', error);
res.status(500).json({ message: 'Internal server error' });
}
});
// Route to get all orders (for demonstration)
app.get('/orders', (req, res) => {
console.log('Order Service: Fetching all orders');
res.status(200).json(orders);
});
// Define the port
const PORT = process.env.PORT || 3002;
app.listen(PORT, () => {
console.log(`Order Service listening on port ${PORT}`);
});
Update package.json scripts:
Open services/order-service/package.json and add these scripts:
// services/order-service/package.json
{
// ... other fields
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
// ... other fields
}
Now, deploy the Order Service. But before deploying, we need to tell Void Cloud about the environment variables it needs.
cd services/order-service
vc deploy
After deployment, Void Cloud will give you a URL for the Order Service (e.g., https://order-service-zzzz.void.app).
Setting Environment Variables for Order Service: This is crucial. The Order Service needs to know the URLs of the User and Product services. We’ll use the URLs you noted earlier.
cd services/order-service
vc env add VOID_USER_SERVICE_URL "https://user-service-xxxx.void.app" --git
vc env add VOID_PRODUCT_SERVICE_URL "https://product-service-yyyy.void.app" --git
The --git flag ensures these environment variables are linked to your Git project and will be available on subsequent deployments. After adding these, Void Cloud might prompt you to redeploy to apply the changes. If not, you can trigger a redeploy: vc deploy.
Step 5: Implement the API Gateway
The API Gateway will be the public face of our microservices. It will receive all client requests and forward them to the appropriate internal service. We’ll use http-proxy-middleware for simple routing.
File: api-gateway/src/index.ts
// api-gateway/src/index.ts
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
const app = express();
app.use(express.json());
// Environment variables for service URLs
const USER_SERVICE_URL = process.env.VOID_USER_SERVICE_URL;
const PRODUCT_SERVICE_URL = process.env.VOID_PRODUCT_SERVICE_URL;
const ORDER_SERVICE_URL = process.env.VOID_ORDER_SERVICE_URL;
if (!USER_SERVICE_URL || !PRODUCT_SERVICE_URL || !ORDER_SERVICE_URL) {
console.error('API Gateway: Missing one or more service URLs in environment variables!');
// In a real app, you might want to gracefully degrade or halt startup
process.exit(1);
}
console.log('API Gateway: Initializing with service URLs:');
console.log(` User Service: ${USER_SERVICE_URL}`);
console.log(` Product Service: ${PRODUCT_SERVICE_URL}`);
console.log(` Order Service: ${ORDER_SERVICE_URL}`);
// Proxy middleware for User Service
app.use('/api/users', createProxyMiddleware({
target: USER_SERVICE_URL,
changeOrigin: true, // Needed for virtual hosted sites
pathRewrite: { '^/api/users': '/users' }, // Rewrite /api/users to /users for the backend
onProxyReq: (proxyReq, req, res) => {
console.log(`API Gateway: Proxying /api/users request to ${USER_SERVICE_URL}${req.url}`);
}
}));
// Proxy middleware for Product Service
app.use('/api/products', createProxyMiddleware({
target: PRODUCT_SERVICE_URL,
changeOrigin: true,
pathRewrite: { '^/api/products': '/products' },
onProxyReq: (proxyReq, req, res) => {
console.log(`API Gateway: Proxying /api/products request to ${PRODUCT_SERVICE_URL}${req.url}`);
}
}));
// Proxy middleware for Order Service
app.use('/api/orders', createProxyMiddleware({
target: ORDER_SERVICE_URL,
changeOrigin: true,
pathRewrite: { '^/api/orders': '/orders' },
onProxyReq: (proxyReq, req, res) => {
console.log(`API Gateway: Proxying /api/orders request to ${ORDER_SERVICE_URL}${req.url}`);
}
}));
// Basic health check endpoint for the gateway itself
app.get('/health', (req, res) => {
res.status(200).send('API Gateway is healthy');
});
// Define the port
const PORT = process.env.PORT || 8080; // Standard port for gateways
app.listen(PORT, () => {
console.log(`API Gateway listening on port ${PORT}`);
});
Update package.json scripts:
Open api-gateway/package.json and add these scripts:
// api-gateway/package.json
{
// ... other fields
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
// ... other fields
}
Now, deploy the API Gateway. Just like the Order Service, it needs environment variables.
cd api-gateway
vc deploy
Note the deployed URL for the API Gateway (e.g., https://api-gateway-aaaa.void.app).
Setting Environment Variables for API Gateway: The API Gateway needs the URLs of all the services it proxies.
cd api-gateway
vc env add VOID_USER_SERVICE_URL "https://user-service-xxxx.void.app" --git
vc env add VOID_PRODUCT_SERVICE_URL "https://product-service-yyyy.void.app" --git
vc env add VOID_ORDER_SERVICE_URL "https://order-service-zzzz.void.app" --git
Again, Void Cloud might prompt for a redeploy, or you can run vc deploy to apply the changes.
Step 6: Testing the Microservices Architecture
With all services deployed and configured, it’s time to test! All requests should now go through your API Gateway URL.
1. Get all users:
curl https://api-gateway-aaaa.void.app/api/users
You should see the mock user data.
2. Get all products:
curl https://api-gateway-aaaa.void.app/api/products
You should see the mock product data.
3. Create a user (optional, our mock data is static):
curl -X POST -H "Content-Type: application/json" -d '{"name": "Charlie Brown", "email": "charlie@example.com"}' https://api-gateway-aaaa.void.app/api/users
(Note: Our mock user service doesn’t persist, so this user won’t be in the list on subsequent fetches.)
4. Create an order (This tests inter-service communication!):
curl -X POST -H "Content-Type: application/json" -d '{"userId": "usr_123", "productId": "prod_a1", "quantity": 2}' https://api-gateway-aaaa.void.app/api/orders
If everything is set up correctly, you should receive a successful order creation response.
Check the logs for your Order Service (vc logs services/order-service) to see the User validated and Product details fetched messages, confirming it communicated with the other services.
5. Get all orders:
curl https://api-gateway-aaaa.void.app/api/orders
You should see the order you just created.
Congratulations! You’ve successfully deployed a microservices architecture on Void Cloud, demonstrating independent services, an API Gateway, and inter-service communication via environment variables.
Mini-Challenge: Add an Inventory Service
Let’s extend our architecture. Imagine we want to manage product stock more dynamically.
Challenge:
- Create a new
Inventory Service(inservices/inventory-service). - This service should expose an endpoint like
/inventory/:productIdthat returns the current stock level for a product. - The
Product Serviceshould no longer hold thestockproperty. Instead, when a request comes to/products/:id, theProduct Serviceshould call the newInventory Serviceto fetch the stock level and combine it with its own product details before returning the full product information. - Ensure the
Order Servicestill works correctly by making sure theProduct Serviceprovides thestockinformation after its internal call to theInventory Service. - Deploy the new service and update existing ones on Void Cloud with necessary environment variables and redeployments.
Hint:
- Follow the pattern of creating a new service (directory,
npm init,vc init,src/index.ts). - The
Product Servicewill need a new environment variable:VOID_INVENTORY_SERVICE_URL. - Remember to
vc env addandvc deployfor each service that changes or is new.
What to Observe/Learn:
- How easy it is to add new services to an existing microservices architecture.
- Chaining of service calls (API Gateway -> Product Service -> Inventory Service).
- The importance of updating environment variables for inter-service communication.
Common Pitfalls & Troubleshooting
Working with distributed systems like microservices introduces new challenges. Here are a few common pitfalls and how to troubleshoot them on Void Cloud:
Incorrect Environment Variables:
- Problem: Services cannot communicate or return
500errors related toundefinedURLs. - Diagnosis: Double-check that
VOID_USER_SERVICE_URL,VOID_PRODUCT_SERVICE_URL, etc., are correctly set for the relevant services. Usevc env ls <project-name>to list environment variables for a specific project. - Solution: Use
vc env add <KEY> <VALUE> --gitand thenvc deploythe affected service.
- Problem: Services cannot communicate or return
Network Issues/Timeouts:
- Problem: Services are deployed, but requests between them time out or fail.
- Diagnosis: Check
vc logs <project-name>for the calling service. Look forECONNREFUSEDorETIMEDOUTerrors. Ensure the target service’s URL is correct and it’s actually running. Sometimes a service might take longer to cold-start. - Solution: Verify URLs. Increase timeouts in the calling service if necessary (though Void Cloud functions usually have generous defaults). If a service is consistently failing, inspect its own logs for startup errors.
Path Rewriting in API Gateway:
- Problem: Client requests to
/api/usersresult in404or unexpected behavior from the User Service. - Diagnosis: The
pathRewriteconfiguration inhttp-proxy-middlewareis crucial. If^/api/usersis not correctly rewritten to/users(or whatever the backend service expects), the backend won’t find the route. - Solution: Carefully review the
pathRewriteregex and replacement string. Test the backend service’s direct URL to ensure its routes work as expected.
- Problem: Client requests to
Distributed Debugging:
- Problem: An error occurs, but it’s hard to pinpoint which service caused it in the chain.
- Diagnosis: Void Cloud’s unified logging (accessible via
vc logs) is your best friend. Look at the logs for the API Gateway, then the service it called, and so on. Add descriptiveconsole.logmessages in your code to trace the flow. - Solution: In production, consider implementing distributed tracing (e.g., using OpenTelemetry, which Void Cloud supports) to get a clear view of requests across services. For now, detailed logging is key.
Summary
In this chapter, we embarked on an exciting journey into the world of microservices with Void Cloud. We covered:
- Understanding Microservices: What they are, why they’re beneficial, and how they contrast with monolithic architectures.
- Void Cloud’s Role: How Void Cloud’s serverless functions, independent deployments, and environment management make it an excellent platform for microservices.
- Architecture Design: We designed a simple e-commerce system with User, Product, Order, and API Gateway services.
- Step-by-Step Implementation: We built each service incrementally using Node.js and TypeScript, deploying them as separate Void Cloud projects.
- Inter-Service Communication: We learned how to enable services to talk to each other using HTTP requests and Void Cloud environment variables.
- API Gateway: We implemented an API Gateway to provide a unified entry point for clients and route requests to the correct backend services.
- Troubleshooting: We discussed common issues like environment variable misconfigurations and distributed debugging strategies.
You’ve now got a solid foundation for building and deploying scalable, resilient microservices on Void Cloud. The ability to break down complex applications into smaller, manageable pieces is a superpower in modern software development!
What’s next? In the upcoming chapters, we’ll delve deeper into advanced topics like integrating databases, real-time communication, and robust monitoring for production-grade applications on Void Cloud. Keep exploring, and happy coding!
References
- Void Cloud Official Documentation: Microservices Best Practices
- Void Cloud Official Documentation: Serverless Functions
- Void Cloud Official Documentation: Environment Variables
- Express.js Official Website
- TypeScript Official Website
- Node-Fetch GitHub Repository
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.