Why this matters
Defining clear service boundaries and reliable contracts prevents distributed monoliths, reduces coordination overhead, and makes changes safer. In a Backend Engineer role, you will:
- Split a large domain into cohesive services with owned data.
- Design HTTP APIs and event contracts that are resilient and versioned.
- Handle compatibility, retries, idempotency, and failure modes.
- Collaborate with other teams using consumer-first contracts.
Concept explained simply
Service boundary = what a service owns and is responsible for: its domain rules, data, and SLAs. Think of it as the fence around a piece of the business.
Contract = how others interact with the service: request/response APIs and asynchronous event schemas. Contracts are promises you must keep compatible as your service evolves.
Mental model
- Picture services as LEGO bricks. Each brick has a clear shape (responsibilities) and ports (contracts). Bricks connect cleanly or not at all.
- If two parts of code always change together, they probably belong in the same brick.
- If a feature requires cross-service transactions, prefer eventual consistency via events or sagas instead of stretching boundaries.
How to find good boundaries
Heuristics you can apply
- Cohesion of behavior: Group rules that belong to the same business capability (e.g., Payments authorizes and captures money; Orders tracks order lifecycle).
- Change together: Components that change for the same reason should live together. If a change regularly requires coordinated deploys across services, your split is off.
- Data ownership: One service is the source of truth for a dataset. Others read via APIs or replicas; they do not write directly.
- Transaction boundaries: If you need atomicity across functions, consider them one service; otherwise use eventual consistency patterns.
- External dependencies: Integrations with distinct SLAs/rate limits (e.g., payment gateways, email providers) often justify a separate boundary.
- Team alignment: Align services to team ownership. Cross-team changes should be API-level, not DB-level.
- Regulatory isolation: PII or sensitive domains can be isolated for compliance and auditability.
Contract design essentials
- API shape: Use clear resources and verbs. Be explicit about required/optional fields.
- Validation: Fail fast with useful error details. 400/422 for validation; 404 for missing; 409 for conflicts.
- Idempotency: Support safe retries for create/update by using an idempotency key and deterministic server behavior.
- Pagination and filtering: Consistent query parameters and stable sort keys.
- Time/outages: Set timeouts. Document retry policies and backoff expectations.
- Rate limits: Communicate limits and remaining quota in headers or fields.
- Security: Authentication, authorization, encryption in transit. Avoid leaking internal identifiers unnecessarily.
- Error model: Structured errors with machine-readable codes and human-friendly messages.
- Versioning: Prefer backward-compatible, additive changes. For breaking changes, introduce a new version and deprecate with a sunset period.
- Events: Name event types clearly, include a schema version, timestamps, ids, and correlation ids. Ensure at-least-once delivery is safe via idempotency.
Quick checklist for your contract
- Resource names and fields are descriptive and stable.
- All non-200 responses have a structured body with code and message.
- Idempotency key supported for non-safe operations.
- Pagination is consistent and documented.
- Backward compatibility plan exists (additive by default, versioned when breaking).
- Events include schemaVersion, eventId, occurredAt, correlationId, and an aggregateId.
- Security and PII handling are explicit.
Worked examples
E-commerce: Orders and Payments
Boundaries:
- Orders Service owns order lifecycle, status, and order lines.
- Payments Service owns payment intents, authorization, capture, refund, and gateway integration.
Contracts:
Orders HTTP endpoints (sample)
{
"POST /orders": {
"request": {"customerId": "uuid", "items": [{"sku": "string", "qty": 2}]},
"idempotency": "Idempotency-Key header",
"responses": {
"201": {"orderId": "uuid", "status": "PENDING"},
"422": {"code": "INVALID_ITEM", "message": "SKU not found"}
}
},
"GET /orders/{orderId}": {"200": {"orderId": "uuid", "status": "CONFIRMED"}}
}
Payments events (sample)
{
"type": "payment.authorized",
"schemaVersion": 1,
"eventId": "uuid",
"occurredAt": "2025-09-13T12:00:00Z",
"correlationId": "uuid-of-order",
"data": {"paymentIntentId": "uuid", "amount": 2599, "currency": "USD"}
}
Flow: Orders creates an order, emits order.created. Payments listens, creates a payment intent, and on authorization emits payment.authorized. Orders updates status upon receiving the payment event. No shared database.
Users and Notifications
Boundaries:
- User Service: profile, preferences, identity linkage.
- Notification Service: email/SMS delivery, templates, provider retries.
Contract sketch
// User emits
{
"type": "user.registered",
"schemaVersion": 1,
"eventId": "uuid",
"data": {"userId": "uuid", "email": "alice@example.com"}
}
// Notification API for delivery status
GET /notifications/{id}
200 {"id":"uuid","status":"DELIVERED"}
404 {"code":"NOT_FOUND","message":"..."}
Notification retries delivery with backoff and idempotency keys per provider request to avoid duplicates.
Travel booking: Inventory and Pricing
Boundaries:
- Inventory: seat/room availability; reservations and release windows.
- Pricing: fare rules and dynamic price calculations.
Contract choices
- Sync API for price quote at search time: GET /prices?route=...&date=...
- Async events for price rule updates: pricing.rule.updated v2 (additive fields only).
- Inventory never calls Pricing DB; it requests prices via API and caches with TTL.
Who this is for
- Backend Engineers moving from monoliths to microservices.
- Developers integrating with external providers or cross-team APIs.
- Tech Leads defining service ownership and collaboration boundaries.
Prerequisites
- Comfortable with HTTP, JSON, and basic REST patterns.
- Basic message queue/event knowledge (publish/subscribe, at-least-once).
- Familiarity with data modeling and transactions.
Learning path
- Map the domain into capabilities and data ownership.
- Draft API and event contracts with idempotency and error models.
- Add resiliency: timeouts, retries, rate limits, and circuit breakers.
- Plan compatibility: additive changes first; version when breaking.
- Validate via consumer-driven contract tests before coding integrations.
Exercises
Do these in order. A quick checklist is included to track yourself.
- Exercise ex1: Propose service boundaries for a food delivery app (merchants, menus, orders, payments, delivery).
- Exercise ex2: Design a create-and-fetch API contract for Orders with idempotency and clear errors.
- Exercise ex3: Evolve an event schema without breaking consumers; plan v1 to v2 rollout.
Checklist: ready to move on?
- I identified data ownership and avoided shared databases.
- My API has idempotency for non-safe operations.
- I defined error codes and structures.
- I documented pagination and filtering.
- I planned a backward-compatible path and deprecation window.
- My events include schemaVersion, eventId, occurredAt, and correlationId.
Common mistakes and self-checks
- Shared DB between services: Smells like tight coupling. Self-check: Can you deploy one service independently without schema coordination?
- Chatty APIs: N+1 cross-service calls. Self-check: Can you batch or pre-compute? Would a read model or cache help?
- Hidden breaking changes: Renaming fields or changing types. Self-check: Would an old client still parse and behave correctly?
- No idempotency: Duplicate charges or orders. Self-check: If the client retries, will the outcome be exactly once?
- Leaky abstractions: Exposing internal DB ids or internal error messages. Self-check: Is the contract expressing business concepts?
Practical projects
- Split a simple monolith shop into two services: Catalog and Orders. Add an API gateway or simple reverse proxy and write contracts.
- Emit order.created and payment.authorized events and build a small dashboard service that projects a read model.
- Introduce a backward-compatible change (add a field) and demonstrate old client compatibility plus a deprecation notice.
Next steps
- Complete the exercises below, then take the Quick Test.
- Iterate your contracts with feedback from a mock "consumer" (another teammate or your future self).
- Note: The Quick Test is available to everyone. Log in to save your progress.
Mini challenge
Design a minimal contract for a Refunds service that receives a refund request and emits refund.completed. Include idempotency, error model, and an event schema with correlation id. Keep it additive-friendly.
Ready for the Quick Test?
When your exercises feel solid and your checklist is all checked, jump into the test to validate your understanding.