Tangy is building your next step
Shaping the lesson around what you want to make.
Tangy is building your next step
Shaping the lesson around what you want to make.
Computer Science
Scale your applications beyond a single server. Learn the core patterns of distributed architectures by building, containerizing, and scaling an event-driven task processing system with an API Gateway, background workers, and shared caching.
Build it yourself, get guided when you are stuck, and leave with proof you can actually show.
Follow the path module by module. The layout keeps the lesson count, your progress, and the module description in one scan.
From Monolith to Microservices: API Gateway & Worker Isolation
3 lessonsDeconstruct a single server into dedicated services to handle high traffic and isolated workloads safely.
Asynchronous Decoupling with Message Queues
4 lessonsTransition from blocking HTTP calls to a non-blocking, event-driven architecture using message queues.
Containerization and Horizontal Scaling
3 lessonsPackage your distributed services and scale them horizontally to handle massive parallel workloads.
Distributed Resilience: Caching and Fault Tolerance
3 lessonsProtect your system against failure, handle network partitions, and optimize reads with shared caches.
Deployment: Sharing Your Project
2 lessonsDeploy your distributed task queue to a free-tier platform and share a live URL others can try.
Public lesson
Right now, our backend is a single block. If a user asks our server to do a heavy, slow task, the entire server freezes up and other users can't even load the homepage.
Did You Know? Netflix's open‑source API gateway, Zuul, routes billions of requests each day and can handle over 100 million requests per minute. It powers Netflix's edge layer, demonstrating how a well‑designed gateway can scale to massive traffic while keeping services isolated.
To understand why we split monolithic systems, let's look at a giant that pioneered this pattern: Netflix.
In its early days, Netflix ran on a monolithic architecture. As millions of users streamed videos worldwide, a single bug in the billing code could take down the video playback system, or a spike in users browsing the movie catalog could crash the authentication service.
To solve this, Netflix broke their monolith apart into hundreds of microservices. Here is how key services interact in their ecosystem:
Each of these services has different characteristics, requirements, and risk profiles:
If they were bundled together, an issue or heavy load in one (e.g., millions of recommendations being calculated) would impact the availability of others (e.g., blocking logins or streaming).
Netflix is famous not just for adopting microservices, but for sharing their technology with the entire world. They have open-sourced a massive portion of their cloud infrastructure tools, allowing thousands of other companies to build and run highly resilient microservices.
Here are the key open-source tools pioneered by Netflix:
| Tool Name | Core Capability | What It Does in Simple Terms |
|---|---|---|
| Eureka | Service Discovery | Keeps a dynamic, real-time "phone book" of where every microservice instance is running (IP addresses and ports) so services can find each other. |
| Zuul | API Gateway | Acts as the front entrance, routing all incoming client requests to the appropriate downstream microservices while handling security and rate limiting. |
| Hystrix | Circuit Breaker | Stops cascading failures. If one service is slow or crashing, Hystrix "trips" a circuit breaker to stop sending traffic to it, returning a safe fallback response instead of freezing the system. |
| Chaos Monkey | Resiliency Testing | Randomly terminates virtual machine instances and services in production to force engineers to build self-healing, highly resilient software. |
| Ribbon | Client-Side Load Balancing | Helps microservices distribute outgoing requests evenly across multiple healthy instances of another service (working closely with Eureka). |
| Spinnaker | Continuous Delivery | A multi-cloud deployment platform used to release and manage software updates quickly, safely, and automatically. |
In a real production environment, these tools act as a coordinated symphony:
You might wonder: why would a multi-billion-dollar company share their proprietary cloud architecture for free?
You just saw how Netflix splits responsibilities across services to prevent a slow or failing component from dragging down the entire platform. Your project faces the same challenge: right now, all HTTP requests flow through a single backend service. If an incoming request triggers a heavy, blocking operation (like a large data transformation or a slow third-party API call), every other request on that service blocks and waits—which means users see slow or unresponsive pages even for lightweight tasks.
To fix this, we're going to apply the same pattern: split your existing backend so that request handling and heavy work live in separate services.
Here's what we'll change:
Because your monorepo is ready, we can run these two services side-by-side. Let's build the Processor and wire them together.
This service will live in apps/processor/src/index.ts. Its only job is to receive raw data, process it, and return the result.
Tasks
Open apps/processor/src/index.ts and set up the Express route. You need to write the logic that takes a string from the request body, converts it to uppercase, and sends it back.
import express from 'express';
const app = express();
const PORT = 5001;
app.use(express.json());
// This endpoint receives the raw data to process
app.post('/jobs', (req, res) => {
const { data } = req.body;
if (!data) {
return res.status(400).json({ error: 'No data provided' });
}
// TODO: Transform the 'data' string to uppercase
const processedData = // ... your code here ...
res.json({
status: 'completed',
result: processedData,
processedAt: new Date().toISOString()
});
});
app.listen(PORT, () => {
console.log(`Processor service running on port ${PORT}`);
});
import express from 'express';
const app = express();
const PORT = 5001;
app.use(express.json());
// This endpoint receives the raw data to process
app.post('/jobs', (req, res) => {
const { data } = req.body;
if (!data) {
return res.status(400).json({ error: 'No data provided' });
}
// TODO: Transform the 'data' string to uppercase
const processedData = // ... your code here ...
res.json({
status: 'completed',
result: processedData,
processedAt: new Date().toISOString()
});
});
app.listen(PORT, () => {
console.log(`Processor service running on port ${PORT}`);
});
Now let’s build the Gateway in apps/gateway/src/index.ts.
The Gateway will accept a client request at /work, extract the payload, and use fetch to pass it off to the Processor service on port 5001. Once the Processor responds, the Gateway sends that result back to the user.
Tasks
Complete the fetch call below to connect the two services.
import express from 'express';
const app = express();
const PORT = 5000;
app.use(express.json());
app.post('/work', async (req, res) => {
const { payload } = req.body;
if (!payload) {
return res.status(400).json({ error: 'No payload provided' });
}
try {
// TODO: Use native fetch to send a POST request to the Processor's /jobs endpoint.
// The Processor is running on port 5001.
const response = await fetch('/* fill in the processor URL */', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: payload }),
});
if (!response.ok) {
throw new Error(`Processor returned status ${response.status}`);
}
// TODO: Parse the JSON response from the Processor
const data = await // ... your code here ...
// Send the processed result back to the client
res.json({
message: 'Gateway successfully routed the job',
processorResponse: data,
});
} catch (error) {
console.error('Error contacting processor:', error);
res.status(502).json({ error: 'Processor service is unreachable' });
}
});
app.listen(PORT, () => {
console.log(`Gateway service running on port ${PORT}`);
});
import express from 'express';
const app = express();
const PORT = 5000;
app.use(express.json());
app.post('/work', async (req, res) => {
const { payload } = req.body;
if (!payload) {
return res.status(400).json({ error: 'No payload provided' });
}
try {
// TODO: Use native fetch to send a POST request to the Processor's /jobs endpoint.
// The Processor is running on port 5001.
const response = await fetch('/* fill in the processor URL */', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: payload }),
});
if (!response.ok) {
throw new Error(`Processor returned status ${response.status}`);
}
// TODO: Parse the JSON response from the Processor
const data = await // ... your code here ...
// Send the processed result back to the client
res.json({
message: 'Gateway successfully routed the job',
processorResponse: data,
});
} catch (error) {
console.error('Error contacting processor:', error);
res.status(502).json({ error: 'Processor service is unreachable' });
}
});
app.listen(PORT, () => {
console.log(`Gateway service running on port ${PORT}`);
});
Tasks
To verify that your services are talking to each other, we need to run both at the same time.
Open two terminal windows.
In the first terminal, start the Processor:
npm run dev --workspace=processor
npm run dev --workspace=processor
(Or your monorepo's equivalent run command, ensuring it runs on port 5001)
In the second terminal, start the Gateway:
npm run dev --workspace=gateway
npm run dev --workspace=gateway
(Ensuring it runs on port 5000)
Tasks
Now, send a request to the Gateway (port 5000) using curl in a third terminal, or your preferred API client (like Postman/Thunder Client):
curl -X POST http://localhost:5000/work \
-H "Content-Type: application/json" \
-d '{"payload": "hello multi-service world"}'
curl -X POST http://localhost:5000/work \
-H "Content-Type: application/json" \
-d '{"payload": "hello multi-service world"}'
If everything is wired correctly, you should receive a response back from port 5000 containing the uppercase version of your payload, generated by the service on port 5001:
{
"message": "Gateway successfully routed the job",
"processorResponse": {
"status": "completed",
"result": "HELLO MULTI-SERVICE WORLD",
"processedAt": "2026-02-05..."
}
}
{
"message": "Gateway successfully routed the job",
"processorResponse": {
"status": "completed",
"result": "HELLO MULTI-SERVICE WORLD",
"processedAt": "2026-02-05..."
}
}
If you get a 502 error, double-check that your Gateway's fetch URL is pointing exactly to http://localhost:5001/jobs.
5 modules · 15 lessons
From Monolith to Microservices: API Gateway & Worker Isolation
Asynchronous Decoupling with Message Queues
Containerization and Horizontal Scaling
Distributed Resilience: Caching and Fault Tolerance
Deployment: Sharing Your Project
Learn by building your own version.
Remix this public project to open the workspace, follow the guided build, and let the AI mentor teach you through the work instead of doing it for you.