Tutorial 5: Service Layer¶
Duration: 1.5 hours
Prerequisites: Tutorials 1-4 completed
What you'll learn: Service actions, commands/queries, authorization, API contracts, rate limiting, and composition
Overview¶
The service layer exposes your domain logic to external consumers (REST APIs, GraphQL, CLI). USL provides:
- Actions: Commands (mutating) and queries (read-only)
- Contracts: Strong input/output types with validation
- Authorization: Role and permission-based access control
- Rate Limiting: Automatic throttling with configurable policies
- Events: Pub/sub for async notifications
- Composition: Chain actions with transaction guarantees
- Code Generation: Automatic REST/GraphQL/gRPC endpoints
Project: Order Management API¶
We'll build a production-ready order management API with authentication, authorization, and monitoring.
Step 1: Basic Service Actions¶
Create the order service:
service OrderService {
// Command: Creates a new order
action CreateOrder(input: CreateOrderInput) -> Result[Order, OrderError]
requires authenticated
permits CreateOrder
rate_limit 10 per minute
effects {
// Validate input
require input.items.len() > 0, "Order must have at least one item"
require input.shippingAddress.isValid(), "Invalid shipping address"
// Calculate pricing
let subtotal = input.items.map(|item| item.price * item.quantity).sum()
let tax = calculateTax(subtotal, input.shippingAddress.state)
let shipping = calculateShipping(input.items, input.shippingAddress)
let total = subtotal + tax + shipping
// Create order entity
let order = Order {
id: generateOrderId(),
customerId: context.user.id,
items: input.items.map(|item| LineItem {
id: generateLineItemId(),
orderId: order.id,
productId: item.productId,
quantity: item.quantity,
price: item.price
}),
subtotal: subtotal,
tax: tax,
shippingCost: shipping,
totalAmount: total,
status: OrderStatus.Pending,
shippingAddress: input.shippingAddress,
billingAddress: input.billingAddress,
createdAt: now(),
confirmedAt: None,
shippedAt: None,
deliveredAt: None
}
// Persist order
let saved = persist(order)?
// Emit event for downstream processing
emit OrderCreated {
orderId: saved.id,
customerId: saved.customerId,
totalAmount: saved.totalAmount,
timestamp: now()
}
// Schedule timeout for unconfirmed orders
schedule(72 hours) {
if !orderConfirmed(saved.id) {
cancelOrder(saved.id, "Order expired")
}
}
Ok(saved)
}
// Query: Get order by ID
action GetOrder(orderId: OrderId) -> Result[Order, OrderError]
requires authenticated
permits ViewOrder
effects {
let order = fetch Order where id == orderId
.ok_or(OrderError.NotFound)?
// Authorization: Can only view own orders unless admin
require
order.customerId == context.user.id ||
context.user.hasRole("Admin"),
"Unauthorized to view this order"
Ok(order)
}
// Query: List user's orders with pagination
action ListMyOrders(
page: Int,
pageSize: Int,
filters: Option[OrderFilters]
) -> Result[PaginatedOrders, OrderError]
requires authenticated
permits ViewOrder
rate_limit 60 per minute
effects {
require page >= 0, "Page must be non-negative"
require pageSize > 0 && pageSize <= 100, "Page size must be 1-100"
let query = fetch Order
where customerId == context.user.id
// Apply optional filters
let filtered = match filters {
Some(f) => {
query
.filter(|o| f.status.map_or(true, |s| o.status == s))
.filter(|o| f.minAmount.map_or(true, |min| o.totalAmount >= min))
.filter(|o| f.maxAmount.map_or(true, |max| o.totalAmount <= max))
.filter(|o| f.dateFrom.map_or(true, |from| o.createdAt >= from))
.filter(|o| f.dateTo.map_or(true, |to| o.createdAt <= to))
}
None => query
}
// Get paginated results
let total = filtered.count()
let orders = filtered
.order_by(|o| o.createdAt desc)
.skip(page * pageSize)
.take(pageSize)
.collect()
Ok(PaginatedOrders {
orders: orders,
page: page,
pageSize: pageSize,
total: total,
hasMore: (page + 1) * pageSize < total
})
}
// Command: Confirm order (after payment)
action ConfirmOrder(orderId: OrderId) -> Result[Order, OrderError]
requires authenticated
permits ConfirmOrder
effects {
let order = fetch Order where id == orderId
.ok_or(OrderError.NotFound)?
// Authorization check
require order.customerId == context.user.id, "Unauthorized"
// State machine transition
order.confirm()? // Calls OrderLifecycle.confirm transition
emit OrderConfirmed {
orderId: order.id,
confirmedAt: now()
}
Ok(order)
}
// Command: Cancel order
action CancelOrder(
orderId: OrderId,
reason: String
) -> Result[Order, OrderError]
requires authenticated
permits CancelOrder
effects {
let order = fetch Order where id == orderId
.ok_or(OrderError.NotFound)?
// Authorization: owner or admin
require
order.customerId == context.user.id ||
context.user.hasRole("Admin"),
"Unauthorized"
// Check if cancellable
require
order.status in [OrderStatus.Pending, OrderStatus.Confirmed],
"Cannot cancel order in current status"
// Execute cancellation
order.cancel()?
order.cancellationReason = Some(reason)
persist(order)?
// Refund if payment was captured
if order.status == OrderStatus.Confirmed {
refundPayment(order)?
}
emit OrderCancelled {
orderId: order.id,
reason: reason,
cancelledAt: now()
}
Ok(order)
}
// Query: Search orders (admin only)
action SearchOrders(
query: String,
filters: OrderFilters
) -> Result[List[Order], OrderError]
requires authenticated
permits AdminSearchOrders
requires hasRole("Admin")
rate_limit 30 per minute
effects {
let results = search Order {
text: query,
fields: ["id", "customerId", "items"],
filters: {
status: filters.status,
dateRange: (filters.dateFrom, filters.dateTo),
amountRange: (filters.minAmount, filters.maxAmount)
},
limit: 100
}
Ok(results)
}
}
Step 2: Input/Output Contracts¶
Define strongly-typed contracts:
// Input contracts
record CreateOrderInput {
items: List[OrderItemInput]
shippingAddress: Address
billingAddress: Address
paymentMethodId: String
invariant has_items {
len(this.items) > 0
}
}
record OrderItemInput {
productId: ProductId
quantity: Int
price: Money
invariant positive_quantity {
this.quantity > 0
}
invariant positive_price {
this.price.amount > 0.0
}
}
record Address {
line1: String
line2: Option[String]
city: String
state: String
zipCode: String
country: String
invariant valid_zip {
len(this.zipCode) >= 5
}
function isValid() -> Bool {
!this.line1.is_empty() &&
!this.city.is_empty() &&
!this.state.is_empty() &&
this.zipCode.matches(r"^\d{5}(-\d{4})?$")
}
}
record OrderFilters {
status: Option[OrderStatus]
dateFrom: Option[Timestamp]
dateTo: Option[Timestamp]
minAmount: Option[Money]
maxAmount: Option[Money]
}
// Output contracts
record PaginatedOrders {
orders: List[Order]
page: Int
pageSize: Int
total: Int
hasMore: Bool
}
// Error types
enum OrderError {
NotFound,
Unauthorized,
InvalidInput(String),
InsufficientStock(List[ProductId]),
PaymentFailed(String),
InvalidTransition(String),
RateLimitExceeded
}
Step 3: Authorization¶
Define permissions and policies:
policy OrderServicePolicy {
// Role definitions
role Customer {
description: "Regular customer"
}
role Admin {
description: "Administrator with full access"
inherits: [Customer]
}
role Support {
description: "Customer support staff"
inherits: [Customer]
}
// Permission definitions
permission CreateOrder {
description: "Can create new orders"
granted_to: [Customer]
}
permission ViewOrder {
description: "Can view order details"
granted_to: [Customer]
}
permission ConfirmOrder {
description: "Can confirm pending orders"
granted_to: [Customer]
}
permission CancelOrder {
description: "Can cancel orders"
granted_to: [Customer, Support, Admin]
}
permission AdminSearchOrders {
description: "Can search all orders"
granted_to: [Admin, Support]
}
// Resource-level authorization
rule "customers can only view own orders" {
action: ViewOrder,
resource: Order,
condition: resource.customerId == context.user.id || context.user.hasRole("Admin")
}
rule "support can view any order" {
action: ViewOrder,
resource: Order,
condition: context.user.hasRole("Support")
}
rule "only admin can search all orders" {
action: AdminSearchOrders,
condition: context.user.hasRole("Admin")
}
}
Step 4: Rate Limiting¶
Configure rate limiting policies:
service OrderService {
// Per-user rate limits
rate_limit_policy default {
window: 1 minute
max_requests: 60
key: context.user.id
}
// Endpoint-specific limits
action CreateOrder(input: CreateOrderInput) -> Result[Order, OrderError]
rate_limit 10 per minute per user
rate_limit 1000 per minute globally
burst: 5 // Allow short bursts
effects { /* ... */ }
// Premium tier bypass
action GetOrder(orderId: OrderId) -> Result[Order, OrderError]
rate_limit match context.user.tier {
"Free" => 60 per minute,
"Premium" => 300 per minute,
"Enterprise" => unlimited
}
effects { /* ... */ }
}
Step 5: Event-Driven Architecture¶
Publish and subscribe to events:
// Event definitions
event OrderCreated {
orderId: OrderId
customerId: UserId
totalAmount: Money
timestamp: Timestamp
}
event OrderConfirmed {
orderId: OrderId
confirmedAt: Timestamp
}
event OrderCancelled {
orderId: OrderId
reason: String
cancelledAt: Timestamp
}
// Event subscribers
service NotificationService {
subscribe OrderCreated => sendOrderConfirmationEmail
subscribe OrderShipped => sendShippingNotification
subscribe OrderDelivered => sendDeliveryNotification
subscribe OrderCancelled => sendCancellationNotification
action sendOrderConfirmationEmail(event: OrderCreated) -> Result[Unit, Error]
effects {
let order = fetch Order where id == event.orderId?
let customer = fetch User where id == event.customerId?
sendEmail {
to: customer.email,
subject: "Order Confirmation #${order.id}",
template: "order_confirmation",
data: {
order: order,
customer: customer
}
}?
Ok(())
}
}
service InventoryService {
subscribe OrderConfirmed => reserveInventory
subscribe OrderCancelled => releaseInventory
action reserveInventory(event: OrderConfirmed) -> Result[Unit, Error]
effects {
let order = fetch Order where id == event.orderId?
for item in order.items {
let product = fetch Product where id == item.productId?
require product.stock >= item.quantity,
Error.InsufficientStock(item.productId)
product.stock -= item.quantity
product.reserved += item.quantity
persist(product)?
}
emit InventoryReserved {
orderId: order.id,
items: order.items
}
Ok(())
}
}
Step 6: Service Composition¶
Chain multiple actions with transaction guarantees:
service OrderService {
// Composite action: Create and confirm order
action CreateAndConfirmOrder(
input: CreateOrderInput,
paymentToken: String
) -> Result[Order, OrderError]
requires authenticated
permits CreateOrder, ConfirmOrder
transactional // All-or-nothing execution
effects {
// Step 1: Create order
let order = CreateOrder(input)?
// Step 2: Process payment
let payment = processPayment {
orderId: order.id,
amount: order.totalAmount,
token: paymentToken,
customerId: context.user.id
}?
// Step 3: Confirm order (triggers state machine transition)
let confirmed = ConfirmOrder(order.id)?
// Step 4: Send confirmation
sendOrderConfirmation(confirmed)?
Ok(confirmed)
}
// Bulk action: Cancel multiple orders
action BulkCancelOrders(
orderIds: List[OrderId],
reason: String
) -> Result[List[Order], OrderError]
requires authenticated
permits AdminCancelOrders
requires hasRole("Admin")
effects {
let cancelled = orderIds.map(|id| {
CancelOrder(id, reason)
}).collect()?
Ok(cancelled)
}
}
Step 7: Testing Services¶
spec OrderServiceTests {
test "can create order with valid input" {
let user = createTestUser()
let context = AuthContext { user: user }
let input = CreateOrderInput {
items: [
OrderItemInput {
productId: ProductId("prod_123"),
quantity: 2,
price: Money(50.00, "USD")
}
],
shippingAddress: createTestAddress(),
billingAddress: createTestAddress(),
paymentMethodId: "pm_test_123"
}
let result = OrderService.CreateOrder(input)
.with_context(context)
.execute()
assert result.is_ok()
let order = result.unwrap()
assert order.customerId == user.id
assert order.items.len() == 1
assert order.status == OrderStatus.Pending
assert eventEmitted(OrderCreated)
}
test "cannot create order without authentication" {
let input = createValidOrderInput()
expect_error(Unauthenticated) {
OrderService.CreateOrder(input).execute()
}
}
test "rate limiting enforced" {
let user = createTestUser()
let context = AuthContext { user: user }
// Make 10 requests (limit per minute)
for i in 0..10 {
let result = OrderService.CreateOrder(createValidOrderInput())
.with_context(context)
.execute()
assert result.is_ok()
}
// 11th request should fail
let result = OrderService.CreateOrder(createValidOrderInput())
.with_context(context)
.execute()
assert result.is_err()
assert result.unwrap_err() == OrderError.RateLimitExceeded
}
test "customers can only view own orders" {
let user1 = createTestUser()
let user2 = createTestUser()
let order = createOrderForUser(user1)
// User1 can view
let result1 = OrderService.GetOrder(order.id)
.with_context(AuthContext { user: user1 })
.execute()
assert result1.is_ok()
// User2 cannot view
let result2 = OrderService.GetOrder(order.id)
.with_context(AuthContext { user: user2 })
.execute()
assert result2.is_err()
assert result2.unwrap_err() == OrderError.Unauthorized
}
}
Step 8: Code Generation¶
Generate REST API:
Generated code:
// Generated REST API (TypeScript/Express)
app.post('/orders', authenticate, async (req, res) => {
try {
const input = CreateOrderInput.parse(req.body);
const result = await orderService.createOrder(input, req.user);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.get('/orders/:id', authenticate, async (req, res) => {
try {
const order = await orderService.getOrder(req.params.id, req.user);
res.json(order);
} catch (error) {
if (error.type === 'NotFound') {
res.status(404).json({ error: 'Order not found' });
} else {
res.status(400).json({ error: error.message });
}
}
});
What You've Learned¶
✅ Service action design (commands and queries)
✅ Input/output contracts with validation
✅ Permission-based authorization
✅ Rate limiting configuration
✅ Event-driven architecture
✅ Service composition with transactions
✅ Service testing strategies
✅ Code generation for REST APIs
Common Patterns¶
CQRS (Command Query Responsibility Segregation)¶
service OrderService {
// Commands (write side)
command CreateOrder(input: CreateOrderInput) -> OrderId { /* ... */ }
command ConfirmOrder(id: OrderId) -> Unit { /* ... */ }
// Queries (read side)
query GetOrder(id: OrderId) -> Order { /* ... */ }
query ListOrders(filters: OrderFilters) -> List[Order] { /* ... */ }
}
Idempotency¶
action CreateOrder(input: CreateOrderInput) -> Result[Order, OrderError]
idempotency_key input.idempotencyKey
effects {
// Check if order already exists with this key
let existing = fetch Order where idempotencyKey == input.idempotencyKey
match existing {
Some(order) => Ok(order), // Return existing order
None => {
// Create new order
let order = /* create order */
persist(order)?
Ok(order)
}
}
}
Exercises¶
- Add Update Action: Create UpdateOrderShippingAddress action
- Add Batch Actions: Create BulkConfirmOrders for admin
- Add Export: Create ExportOrdersToCSV query
- Add Webhooks: Subscribe external systems to order events
Next Steps¶
Continue to Tutorial 6: Spec Testing to learn comprehensive testing strategies.