> For the complete documentation index, see [llms.txt](https://fastapi-docs.duonghuuphuc.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://fastapi-docs.duonghuuphuc.com/unit-19-building-and-communicating-microservices.md).

# Unit 19: Building and Communicating Microservices

Build multiple FastAPI microservices and practice REST-based service-to-service communication, including basic timeout and failure handling.

### 1. Scenario

In previous units, we developed APIs inside individual FastAPI applications. In this unit, we extend that knowledge by building **multiple independent services** that communicate through HTTP.

We will build:

* **Product Service** — provides product information.
* **Order Service** — creates orders and requests product information from Product Service.

The architecture is:

```
Client
   │
   │ POST /orders
   ▼
Order Service :8001
   │
   │ GET /products/{product_id}
   ▼
Product Service :8000
```

The key idea is:

> The Order Service does not directly access Product Service's internal data. It communicates through Product Service's API.

***

## 2. Project Structure

Create the following structure:

```
unit19-microservices/
│
├── product_service/
│   └── main.py
│
└── order_service/
    └── main.py
```

Each folder represents an independent microservice.

***

## 3. Step 1 — Build the Product Service

Open:

```
product_service/main.py
```

Create the FastAPI application:

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="Product Service")
```

Define the product model:

```python
class Product(BaseModel):
    product_id: int
    product_name: str
    price: float
    stock: int
```

Create a sample product dataset:

```python
products = [
    {
        "product_id": 1,
        "product_name": "Laptop",
        "price": 1200.0,
        "stock": 10
    },
    {
        "product_id": 2,
        "product_name": "Wireless Mouse",
        "price": 25.5,
        "stock": 50
    },
    {
        "product_id": 3,
        "product_name": "Mechanical Keyboard",
        "price": 75.0,
        "stock": 20
    }
]
```

Add an endpoint to return all products:

```python
@app.get("/products", response_model=list[Product])
def get_products():
    return products
```

Add another endpoint to return one product:

```python
@app.get("/products/{product_id}", response_model=Product)
def get_product(product_id: int):
    for product in products:
        if product["product_id"] == product_id:
            return product

    raise HTTPException(
        status_code=404,
        detail="Product not found"
    )
```

The Product Service now provides:

```
GET /products
GET /products/{product_id}
```

***

## 4. Step 2 — Run the Product Service

From the `product_service` folder, run:

```bash
uvicorn main:app --reload --port 8000
```

The service runs at:

```
http://127.0.0.1:8000
```

Test:

```
GET /products
```

and:

```
GET /products/1
```

A successful response should look like:

```json
{
    "product_id": 1,
    "product_name": "Laptop",
    "price": 1200.0,
    "stock": 10
}
```

Also test:

```
GET /products/999
```

The response should be:

```
404 Not Found
```

***

## 5. Step 3 — Start the Order Service

Open:

```
order_service/main.py
```

Create the application:

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import httpx

app = FastAPI(title="Order Service")
```

Define the location of Product Service:

```python
PRODUCT_SERVICE_URL = "http://127.0.0.1:8000"
```

This URL will be used by Order Service to call Product Service.

***

## 6. Step 4 — Define the Order Models

Create the request model:

```python
class OrderCreate(BaseModel):
    customer_name: str
    product_id: int
    quantity: int = Field(gt=0)
```

The client only sends:

```
customer_name
product_id
quantity
```

Create the response model:

```python
class Order(BaseModel):
    order_id: int
    customer_name: str
    product_id: int
    product_name: str
    quantity: int
    unit_price: float
    total_amount: float
```

Notice that the client does not provide:

```
product_name
unit_price
```

The Order Service will obtain these values from Product Service.

Create in-memory order storage:

```python
orders = []
```

***

## 7. Step 5 — Add GET /orders

Add:

```python
@app.get("/orders", response_model=list[Order])
def get_orders():
    return orders
```

This endpoint returns all orders currently stored by Order Service.

***

## 8. Step 6 — Call Product Service from Order Service

The main concept of this unit is **service-to-service communication**.

Suppose Order Service needs information about product `1`.

It can send:

```python
response = httpx.get(
    "http://127.0.0.1:8000/products/1"
)
```

The HTTP response includes:

```python
response.status_code
```

and the JSON body can be read using:

```python
product = response.json()
```

For example:

```python
print(product["product_name"])
print(product["price"])
print(product["stock"])
```

Order Service can now use data returned by Product Service.

***

## 9. Step 7 — Implement POST /orders

Add:

```python
@app.post("/orders", response_model=Order)
def create_order(order: OrderCreate):

    product_url = (
        f"{PRODUCT_SERVICE_URL}/products/{order.product_id}"
    )

    response = httpx.get(product_url)

    if response.status_code == 404:
        raise HTTPException(
            status_code=400,
            detail="Invalid product"
        )

    product = response.json()

    if order.quantity > product["stock"]:
        raise HTTPException(
            status_code=400,
            detail="Insufficient product stock"
        )

    new_order = {
        "order_id": len(orders) + 1,
        "customer_name": order.customer_name,
        "product_id": product["product_id"],
        "product_name": product["product_name"],
        "quantity": order.quantity,
        "unit_price": product["price"],
        "total_amount": product["price"] * order.quantity
    }

    orders.append(new_order)

    return new_order
```

The important line is:

```python
response = httpx.get(product_url)
```

The Order Service is acting as an HTTP client of Product Service.

***

## 10. Step 8 — Run Both Services

Keep Product Service running on port `8000`.

Open another terminal and run Order Service:

```bash
uvicorn main:app --reload --port 8001
```

We now have:

```
Product Service → http://127.0.0.1:8000
Order Service   → http://127.0.0.1:8001
```

The architecture becomes:

```
                  HTTP
Order Service ─────────────► Product Service
    :8001                        :8000
```

***

## 11. Step 9 — Create an Order

Send:

```
POST /orders
```

to Order Service.

Request body:

```json
{
    "customer_name": "Nguyen Van An",
    "product_id": 1,
    "quantity": 2
}
```

The communication flow is:

```
1. Client
      │
      │ POST /orders
      ▼
2. Order Service
      │
      │ GET /products/1
      ▼
3. Product Service
      │
      │ Product information
      ▼
4. Order Service
      │
      │ Validate stock
      │ Calculate total
      │ Create order
      ▼
5. Client
```

Product Service returns:

```json
{
    "product_id": 1,
    "product_name": "Laptop",
    "price": 1200.0,
    "stock": 10
}
```

Order Service then creates:

```json
{
    "order_id": 1,
    "customer_name": "Nguyen Van An",
    "product_id": 1,
    "product_name": "Laptop",
    "quantity": 2,
    "unit_price": 1200.0,
    "total_amount": 2400.0
}
```

The client did not send the product name or price. These values came from Product Service.

***

## 12. Step 10 — Test an Invalid Product

Send:

```json
{
    "customer_name": "Nguyen Van An",
    "product_id": 999,
    "quantity": 1
}
```

Order Service sends:

```
GET /products/999
```

Product Service returns:

```
404 Not Found
```

Order Service returns:

```json
{
    "detail": "Invalid product"
}
```

This demonstrates that a service can process and transform errors returned by another service.

***

## 13. Step 11 — Test Insufficient Stock

Send:

```json
{
    "customer_name": "Nguyen Van An",
    "product_id": 2,
    "quantity": 100
}
```

Product Service reports:

```
stock = 50
```

Order Service checks:

```python
if order.quantity > product["stock"]:
```

and returns:

```json
{
    "detail": "Insufficient product stock"
}
```

***

## 14. Step 12 — Handle Service Unavailability

Currently, if Product Service stops running, this line:

```python
response = httpx.get(product_url)
```

will cause a communication error.

Improve it using:

```python
try:
    response = httpx.get(
        product_url,
        timeout=3.0
    )

except httpx.RequestError:
    raise HTTPException(
        status_code=503,
        detail="Product Service is unavailable"
    )
```

Now stop Product Service and send another:

```
POST /orders
```

Order Service should return:

```
503 Service Unavailable
```

This demonstrates an important property of distributed systems:

> A service can fail because another service it depends on is unavailable.

***

## 15. Step 13 — Handle Unexpected Responses

We should also handle other errors returned by Product Service.

Use:

```python
try:
    response = httpx.get(
        product_url,
        timeout=3.0
    )

except httpx.RequestError:
    raise HTTPException(
        status_code=503,
        detail="Product Service is unavailable"
    )


if response.status_code == 404:
    raise HTTPException(
        status_code=400,
        detail="Invalid product"
    )


if response.status_code != 200:
    raise HTTPException(
        status_code=502,
        detail="Error communicating with Product Service"
    )


product = response.json()
```

We now distinguish:

| Situation                   | Response                  |
| --------------------------- | ------------------------- |
| Product exists              | Continue processing       |
| Product does not exist      | `400 Bad Request`         |
| Product Service unavailable | `503 Service Unavailable` |
| Unexpected response         | `502 Bad Gateway`         |

***

## 16. Complete Product Service

`product_service/main.py`

{% code overflow="wrap" %}

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel


app = FastAPI(title="Product Service")


class Product(BaseModel):
    product_id: int
    product_name: str
    price: float
    stock: int


products = [
    {
        "product_id": 1,
        "product_name": "Laptop",
        "price": 1200.0,
        "stock": 10
    },
    {
        "product_id": 2,
        "product_name": "Wireless Mouse",
        "price": 25.5,
        "stock": 50
    },
    {
        "product_id": 3,
        "product_name": "Mechanical Keyboard",
        "price": 75.0,
        "stock": 20
    }
]


@app.get("/products", response_model=list[Product])
def get_products():
    return products


@app.get("/products/{product_id}", response_model=Product)
def get_product(product_id: int):
    for product in products:
        if product["product_id"] == product_id:
            return product

    raise HTTPException(
        status_code=404,
        detail="Product not found"
    )
```

{% endcode %}

***

## 17. Complete Order Service

`order_service/main.py`

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import httpx


app = FastAPI(title="Order Service")


PRODUCT_SERVICE_URL = "http://127.0.0.1:8000"


class OrderCreate(BaseModel):
    customer_name: str
    product_id: int
    quantity: int = Field(gt=0)


class Order(BaseModel):
    order_id: int
    customer_name: str
    product_id: int
    product_name: str
    quantity: int
    unit_price: float
    total_amount: float


orders = []


@app.get("/orders", response_model=list[Order])
def get_orders():
    return orders


@app.post("/orders", response_model=Order)
def create_order(order: OrderCreate):

    product_url = (
        f"{PRODUCT_SERVICE_URL}/products/{order.product_id}"
    )

    try:
        response = httpx.get(
            product_url,
            timeout=3.0
        )

    except httpx.RequestError:
        raise HTTPException(
            status_code=503,
            detail="Product Service is unavailable"
        )

    if response.status_code == 404:
        raise HTTPException(
            status_code=400,
            detail="Invalid product"
        )

    if response.status_code != 200:
        raise HTTPException(
            status_code=502,
            detail="Error communicating with Product Service"
        )

    product = response.json()

    if order.quantity > product["stock"]:
        raise HTTPException(
            status_code=400,
            detail="Insufficient product stock"
        )

    new_order = {
        "order_id": len(orders) + 1,
        "customer_name": order.customer_name,
        "product_id": product["product_id"],
        "product_name": product["product_name"],
        "quantity": order.quantity,
        "unit_price": product["price"],
        "total_amount": product["price"] * order.quantity
    }

    orders.append(new_order)

    return new_order
```

***

## 18. Practice Tasks

After completing the tutorial, perform the following tasks.

### Task 1 — Add GET /orders/{order\_id}

Implement:

```
GET /orders/{order_id}
```

Requirements:

* Return the requested order.
* Return `404 Not Found` if the order does not exist.

***

### Task 2 — Add Product Category

Add:

```
category
```

to Product Service.

Example:

```json
{
    "product_id": 1,
    "product_name": "Laptop",
    "category": "Electronics",
    "price": 1200.0,
    "stock": 10
}
```

Modify Order Service so that the created order also contains the category.

The category must be obtained through the Product Service API.

***

### Task 3 — Observe a Service Failure

Perform the following sequence:

1. Run both services.
2. Create an order successfully.
3. Stop Product Service.
4. Try to create another order.
5. Observe the `503 Service Unavailable` response.
6. Restart Product Service.
7. Create another order.

Explain why Order Service cannot complete the operation while Product Service is unavailable.

***

## 19. Key Takeaways

* A microservices system consists of independently running services.
* Each service is responsible for its own business capability.
* Services should communicate through APIs instead of directly accessing each other's internal data.
* `httpx` can be used for synchronous REST-based service-to-service communication.
* One service may depend on another service during request processing.
* Network communication introduces new failure cases such as unavailable services and timeouts.
* Basic error handling is necessary when one microservice calls another.
