Oct 20, 20267 min read

Service-Oriented Architecture: An Ecommerce Example

Follow an order through separate services for catalog, payments, inventory, and notifications.

Service-oriented architecture (SOA) organizes software into services that provide business functions through defined interfaces. A retailer's website and customer-support tools can use the same ordering or payment service, rather than each implementing those functions separately.

This example follows a customer placing an order. It shows what each service does, how information passes between them, and how the order progresses when a payment or stock reservation fails.

We'll also look at how Encore runs the services and their declared infrastructure locally, then provisions and deploys them in your own AWS or GCP account.

The services in the store

ServiceWhat it doesData it manages in this example
CatalogProvides product details and current pricesProducts and pricing rules
OrdersAccepts purchases and tracks their statusOrders and line items
PaymentsHandles payment authorization, capture, and refunds through a payment providerPayment attempts and provider references
InventoryTracks available stock and reserves itemsStock levels and reservations
NotificationsSends order updatesTemplates and delivery attempts

The website uses these services to place an order. A customer-support application could use Orders to look up the same purchase, or Payments to request a refund. The services expose those operations through their interfaces, so callers can use them without directly changing their database records.

Orders saves the price agreed at checkout with the order. Catalog can later change a product's price without changing what that customer purchased. Inventory handles stock reservations, and Payments maintains the connection to the payment provider.

What happens when a customer places an order?

The client submits the items to Orders. Orders checks the request, gets current product information from Catalog, and stores a pending order. It returns an order ID that the client can use to check progress.

Orders then publishes an OrderPlaced event. Inventory receives it and tries to reserve the items. Payments receives it through a separate subscription and asks the payment provider to authorize the amount. These actions can finish in either order.

Each service publishes its result. Orders confirms the purchase when it has both a successful reservation and a payment authorization. Notifications receives OrderConfirmed and sends the customer a confirmation email.

Client → Orders → Catalog │ └─ OrderPlaced ─┬→ Inventory → StockReserved / StockRejected └→ Payments → PaymentAuthorized / PaymentFailed Orders → OrderConfirmed → Notifications

The initial HTTP request doesn't wait for that whole sequence. The pending order records the work in progress, and the client can retrieve its updated status later. In this example, authorization reserves the payment; capturing the funds can be a later step tied to fulfillment.

What happens when a step fails?

Suppose Inventory reserves the items but the payment is declined. Orders cancels the purchase and requests release of the reservation. If the payment is authorized but stock is unavailable, it asks Payments to void the authorization.

These follow-up operations undo completed work in another service. They are often called compensating actions. The sequence of local transactions and compensating actions is a saga.

Results receivedWhat Orders does
Stock reserved; payment still pendingKeeps the order pending
Payment authorized; stock still pendingKeeps the order pending
Stock reserved and payment authorizedConfirms the order and publishes OrderConfirmed
Payment failed after stock reservationCancels the order and requests release of the reservation
Stock rejected after payment authorizationCancels the order and requests that Payments void the authorization
Duplicate or late resultProcesses the result once, keeps canceled orders canceled, and compensates any late success

Orders also needs a way to resolve work that never finishes. A pending order can expire after the allowed processing time, triggering cancellation and any necessary compensation. A result arriving after cancellation is handled according to the canceled state, so a late authorization does not confirm the purchase again.

Notifications can retry delivery without changing the order's payment or stock status. A failed email leaves a confirmed order confirmed.

What information goes in an event?

An OrderPlaced event carries the purchase details needed to start processing:

{ "eventId": "evt-order-1042", "orderId": "order-1042", "currency": "USD", "totalMinorUnits": 4990, "items": [{ "sku": "book-7", "quantity": 2 }] }

The amount is stored in integer minor units, so 4990 represents USD 49.90. The event records the amount agreed at purchase time. Payments uses the order reference to find the payment details through the application's authorized payment integration; raw card credentials are not included in the event.

The event format is a contract between the services. Adding an optional field can let existing consumers continue working, while changing what an existing field means can require coordinated changes. Keeping the event separate from the Orders database schema lets the service change its storage without changing every consumer.

Handling retries without repeating a purchase

Messages and requests can be retried. Payments might receive the same order event twice, or a request to the payment provider might time out after the provider has already processed it.

Each service uses a stable identifier for the operation and records the outcome. For changes within its own database, it can save the result and deduplication key in one transaction. Repeating that operation returns the recorded outcome or leaves the completed state unchanged.

An external payment call also uses the provider's idempotency mechanism. If the request times out, Payments can retry with the same operation key rather than create another authorization. The same principle applies to releasing a stock reservation or voiding an authorization more than once.

Saving the order and publishing its event

Orders can fail after saving a purchase but before publishing OrderPlaced. Without a recovery mechanism, the database contains a pending order that Inventory and Payments never hear about.

A transactional outbox saves the event in the same database transaction as the order. A publisher then reads the outbox and delivers the event, retrying when necessary. Consumers continue to handle duplicate delivery because the publisher can fail after sending an event but before recording it as sent.

Encore's transactional outbox guide shows this pattern for an Encore.go application. It keeps the database write and the intent to publish together, so event delivery can resume after a failure.

Following an order across services

An order ID connects the business records and logs produced by each service. Distributed tracing shows the requests and message processing involved in a particular attempt, including where time was spent or a call failed.

Orders can also expose how long purchases have remained pending and which result each is waiting for. That lets the team investigate a stuck checkout even when the individual services are responding normally.

How does this relate to microservices?

SOA emphasizes services that expose reusable business capabilities, often across several applications. A payment service used by the store and customer-support tools is one example. Microservices focus on smaller services with independent deployment and operation within an application.

The example here uses asynchronous messaging and service-managed data, practices also common in microservices. They are implementation choices for this store rather than requirements every SOA system must follow. Some SOA environments use broader services and centralized integration middleware.

Building the service infrastructure with Encore

With Encore, the application declares its services, databases, and messaging in code. The platform uses those declarations to run the application locally and provision the infrastructure in your own AWS or GCP account.

For this store, Orders declares the topic used to announce a purchase. Inventory and Payments declare separate subscriptions, so both receive each event. Each service can also declare the database it uses for its own records.

Application code passes through Encore to infrastructure in your own AWS or GCP account.
Encore provisions application infrastructure from declarations in code, in your own AWS or GCP account.

The topic declaration in TypeScript defines the event shape and its delivery guarantee:

import { Topic } from "encore.dev/pubsub"; export interface OrderPlaced { eventId: string; orderId: string; currency: string; totalMinorUnits: number; items: { sku: string; quantity: number }[]; } export const orderPlaced = new Topic<OrderPlaced>("order-placed", { deliveryGuarantee: "at-least-once", });

Inventory and Payments each add a subscription with a handler for their part of the workflow. Those handlers perform the reservation or authorization and publish the result. The at-least-once guarantee means they must handle retries using the stable identifiers described above.

Developers can run the services and local resources together with encore run and inspect requests and event processing in the local dashboard. The same declarations drive cloud provisioning and deployment, keeping the resource configuration with the application as the order flow changes.

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

Frequently asked questions

Does every service need its own physical database server?

No. In this example each service controls its data, but that does not require a separate physical server for each database. The deployment can use shared database infrastructure with separate databases and access permissions.

Does service-oriented architecture require an enterprise service bus?

No. An enterprise service bus is one way to connect services and handle integration. Services can also communicate through direct APIs and messaging, as in the order example.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders