Designing Microservices: A Hands-On CloudOrder API Blueprint (Part 1)

Ankit Sharma
0

1) Introduction

Why CloudOrder: One scenario → many blogs

I’ve tried blogging before (I even wrote a few posts back in 2024), but like most of us, I couldn’t stay consistent. This time I want to do it differently.

Instead of writing random Azure tutorials, I’m building one small project called CloudOrder and using it across multiple posts. Same business scenario, but we evolve it step by step — storage, compute, UI, security, API management, events, everything.

The reason is simple: cloud learning becomes much easier when there’s a real system behind it. You make better decisions, and you also learn what breaks when you change one part of the architecture.


What we’ll build in v1

In this first part, I’m not writing code yet. I’m only locking the blueprint — entities, API shape, and storage approach. I’ve learned the hard way that if you skip this, you end up redesigning things halfway through the build.

In v1, CloudOrder will have three core entities:

  • Product – id, name, image_url, price, stock_level
  • Customer – id, email, address
  • Order – items, amount, status lifecycle

For persistence, I’m starting with Azure Table Storage. Not because it’s the “best database”, but because it’s lightweight and fast to set up. Also, it gives me a clean reason to write a future blogpost on migrating this to Cosmos DB or Azure SQL.

In the next part, we’ll implement the API and run it locally. Later in this series, I’ll deploy the same service using App Service, Container Apps, AKS, and Functions — and once events are in place, I’ll add a small Python consumer service too.



2) Business Scenario

Simple, but real, use case

CloudOrder is based on a workflow we’ve all seen a hundred times: browse a product, place an order, and track its status. I like this scenario because it’s simple enough to build quickly, but realistic enough to grow into a proper microservices-style system.

The first version of CloudOrder will focus only on the core flow:

  • Product – what the customer wants to buy
  • Customer – who is placing the order and where the order needs to go
  • Order – the transaction record - items, amount, and status

That’s it. No fancy payments, no discounts, no carts, no enterprise complexity - for now. The idea is to keep the foundation clean so we can evolve it naturally in later blogs.


Why this scenario works for learning microservices

When people say microservices, the discussions usually become theoretical very quickly. But in the real world, microservices are mostly about one thing: independent change. You want services to evolve without breaking each other.

CloudOrder gives us a clean path to explore that. For example:

  • Product can become its own service later – catalog, pricing, inventory updates
  • Order can become its own service – order creation, status changes, auditing
  • Customer can become its own service – profile, wishlist, address management and saved payment methods

And because this is an Azure-focused series, the same CloudOrder API will also help us explore practical cloud decisions:

  • Compute choices on Azure – App Service vs Container Apps vs AKS vs Functions
  • Frontend hosting – Static Web Apps vs App Service vs Blob static website
  • API exposure – Azure API Management (APIM)
  • Event-driven workflows – OrderCreated → notification / inventory update
  • Polyglot services – a Python worker consuming events from Azure messaging

The goal of this series is not to build the perfect architecture right from the get go. The goal is to build something small, learn by doing, and keep improving it one decision at a time.


3) CloudOrder Core Entities

Before writing even a single line of controller code, I want to be clear about the data model. A surprising number of projects go sideways because the domain is not defined early enough.

All entities in CloudOrder will use GUIDs as IDs. It’s not glamorous, but it’s practical. GUIDs keep things simple when services scale independently or when we migrate storage later.


Product

Product is intentionally minimal in v1. I only want enough fields to support a real UI later and a realistic ordering flow.

  • id – GUID
  • name – product name
  • image_url – image link (useful later for UI/CDN scenarios)
  • price – unit price
  • stock_level – available stock

Example:

{
  "id": "d2f2b2c8-6cf5-4d1f-b8b3-07f57d8c33ce",
  "name": "Wireless Mouse",
  "image_url": "https://example.com/images/mouse.png",
  "price": 499.00,
  "stock_level": 120
}

Customer

Customer details are also kept simple. Authentication and login is intentionally out of scope for v1 — we’ll deal with that later when we add APIM and OAuth.

  • id – GUID
  • email – customer email
  • address – nested address object

Address fields – line1, city, state, country, postal_code

Example:

{
  "id": "8c7616b5-7dbb-4e7f-9e0a-1f847355e67b",
  "email": "holmes@sher-lock.com",
  "address": {
    "line1": "221B Baker Street",
    "city": "London",
    "state": "London",
    "country": "UK",
    "postal_code": "NW1"
  }
}

Order

Order is the center of everything. It connects a customer to one or more products, and it gives us a clean way to introduce event-driven patterns later (OrderCreated, OrderShipped, etc.).

  • id – GUID
  • customer_id – GUID of the customer placing the order
  • items – list of order line items
  • amount – total computed amount
  • status – order state (Created/Confirmed/Shipped/Delivered/Cancelled)

Each order item will contain:

  • product_id – GUID
  • quantity – number of units
  • price – unit price at the time of ordering

Example:

{
  "id": "b7dbf0a8-89b2-4e9d-9a2f-1d8db4e2d4d7",
  "customer_id": "8c7616b5-7dbb-4e7f-9e0a-1f847355e67b",
  "items": [
    {
      "product_id": "d2f2b2c8-6cf5-4d1f-b8b3-07f57d8c33ce",
      "quantity": 2,
      "price": 499.00
    }
  ],
  "amount": 998.00,
  "status": "Created"
}


4) Key Decisions

Now that the scenario and core entities are clear, I want to call out a few decisions we’re making early. None of these are one true way, but they keep the project consistent and make future blogs easier to build on.


GUIDs for IDs

I’m using GUIDs for IDs across Product, Customer, and Order. It’s a boring choice, but it works well once you start thinking in distributed systems terms.

  • We don’t want a central ID generator or database sequence.
  • Services can create records independently without coordination.
  • If we split CloudOrder into multiple services later, GUIDs still work without redesign.

The only downside is that GUIDs are not human readable, but for a backend project like this, that trade-off is acceptable.


Keep the API small but extensible

My goal for v1 is not to build a complete e-commerce system. It’s to build a clean foundation. If we overbuild early, we end up spending time on features that don’t add learning value.

  • Small – just Products, Customers, Orders.
  • Realistic – enough fields to support an actual UI and real API payloads.
  • Extensible – easy to add event publishing, API management, and frontend hosting later.

This is also important because later I want to deploy the same API on different Azure compute services. A smaller app makes those comparisons cleaner.


Start with Azure Table Storage

For persistence, I’m starting with Azure Table Storage. Not because it’s the best choice, but because it’s lightweight and forces good habits early.

  • Simple setup, low overhead, quick to validate the end-to-end flow.
  • Table Storage forces you to think about PartitionKey and RowKey up front.
  • Later we’ll migrate this same CloudOrder API to Cosmos DB or Azure SQL.

The migration part is important for me because that’s what happens in real systems too — you start with something simple, and you evolve when requirements grow.

Also, using Table Storage first makes the comparison much more interesting later, because Cosmos DB and SQL bring very different modeling and query capabilities.


5) Azure Table Storage Modeling

Once we choose Azure Table Storage, the most important thing is to be intentional about how we model data. Table Storage is not like SQL where you can filter on any column freely. Here, good design comes down to two fields: PartitionKey and RowKey.

In CloudOrder v1, we’ll keep things simple: one storage account, three tables, and predictable query patterns.


Tables: Products, Customers & Orders

We will create three separate tables in Azure Table Storage:

  • Products – stores Product entities
  • Customers – stores Customer entities
  • Orders – stores Order entities (including line items)

Even though Table Storage is schema-less, separating tables keeps the design clean and avoids mixing unrelated data. Later, if we split services, this separation helps map naturally to service ownership.


PartitionKey + RowKey strategy

The PartitionKey controls distribution and query efficiency, and the RowKey uniquely identifies the record within that partition. The best approach is usually: pick PartitionKey based on how you plan to query the data.

Here is the modeling approach for CloudOrder v1:

  • Products table – PartitionKey: PRODUCT, RowKey: {productId}
  • Customers table – PartitionKey: CUSTOMER, RowKey: {customerId}
  • Orders table – PartitionKey: {customerId}, RowKey: {orderId}

For Products and Customers, I’m using a constant PartitionKey because the main access pattern is lookup by ID. For Orders, I’m using customerId as the PartitionKey because a very common query is: show me all my orders.


Query patterns we want to support

Before writing code, it’s useful to list down what queries the API needs to support. This is the main reason we decided the PartitionKey strategy above.

  • Get a Product by ID – PartitionKey: PRODUCT, RowKey: productId
  • Get a Customer by ID – PartitionKey: CUSTOMER, RowKey: customerId
  • Get an Order by ID – PartitionKey: customerId, RowKey: orderId
  • List all Orders for a Customer – query by PartitionKey = customerId

Notice the pattern: we’re designing everything so that important queries always have a PartitionKey available. That keeps queries fast and avoids expensive scans.

One small trade-off here is that list all orders across all customers is not efficient in this model. But that is fine for v1, because our focus is building a clean ordering flow, not analytics.


6) API Contract

At this point, we have a clear domain model. Now we need to decide how the APIs will be structured. Since this is a microservices-style series, I’m intentionally keeping the services independent from day one.

That means CloudOrder v1 will have three APIs:

  • CloudOrder.Product.Api – product catalog and stock
  • CloudOrder.Customer.Api – customer profile and address
  • CloudOrder.Order.Api – order creation and status tracking

Each API will own its own data and will not directly read/write another service’s storage. Cross-service workflows will be handled later using synchronous calls or event-driven communication, depending on the scenario and business requirement.


Product endpoints (CloudOrder.Product.Api)

  • POST /api/products – create a product
  • GET /api/products – list products
  • GET /api/products/{productId} – get product by ID
  • PATCH /api/products/{productId}/stock – update stock level

Customer endpoints (CloudOrder.Customer.Api)

  • POST /api/customers – create a customer
  • GET /api/customers/{customerId} – get customer by ID

Order endpoints (CloudOrder.Order.Api)

Orders are stored in Azure Table Storage using customerId as the PartitionKey. So even in the API design, we keep customerId as a required filter for efficient queries.

  • POST /api/orders – create a new order
  • GET /api/orders?customerId={customerId} – list orders for a customer
  • GET /api/orders/{orderId}?customerId={customerId} – get a specific order
  • PATCH /api/orders/{orderId}/status?customerId={customerId} – update order status

Status flow basics

  • Created – order created and accepted by the system
  • Confirmed – order validated/confirmed
  • Shipped – order dispatched for delivery
  • Delivered – order successfully delivered
  • Cancelled – order cancelled

In v1, status transitions will be updated via API calls. In later parts of this series, we’ll make this more realistic using event-driven workflows (for example, inventory reservation and shipping updates). (example: OrderCreated → inventory reservation → order confirmation).


7) Solution Structure

Since we’re building CloudOrder as multiple independent services, the repository structure needs to stay clean. I want each service to be deployable independently, while still keeping a single repo easy to navigate.

The layout below is simple enough for a hands-on project, but also realistic enough to scale into a bigger system.


Projects

CloudOrder v1 will have these projects:

  • CloudOrder.Contracts – shared contracts (DTOs, interfaces, event models)
  • CloudOrder.Product.Api – Product service API
  • CloudOrder.Customer.Api – Customer service API
  • CloudOrder.Order.Api – Order service API

The key rule here is: Contracts can be shared, but data stores are not. Each service owns its own storage and business logic.

I’ll keep CloudOrder.Contracts limited to things like:

  • request/response models (DTOs)
  • shared enums (example: OrderStatus)
  • interfaces (example: event publisher contract)
  • event payloads (we’ll use them later when we add Service Bus)

This avoids tight coupling and keeps the system aligned with microservices principles.


Config approach (env vars, appsettings)

Each service will have its own configuration. In local development we can use appsettings.json, and for deployment we can move configuration to environment variables.

In v1, the main config requirement is storage connectivity for Azure Table Storage. Later, we’ll move toward stronger security patterns (for example, managed identity instead of connection strings).


8) What’s Next

In this post, we focused only on design: the business scenario, entities, storage modeling, and the API contract. The next step is to actually build and run these services.

  • Part 2 – implement CloudOrder.Product.Api, CloudOrder.Customer.Api, and CloudOrder.Order.Api and run them locally
  • Later in the series – deploy the same services using App Service, Container Apps, AKS, and Functions
  • Future upgrade – add event-driven workflows (Service Bus) and a Python consumer service for background processing

The idea is to keep CloudOrder realistic: services stay independent, data stays owned by the service, and workflows evolve gradually as we add more cloud capabilities.


Post a Comment

0 Comments

Post a Comment (0)
3/related/default