Start with boundaries, not services
The fastest way to create a distributed monolith is to draw service boundaries around database tables or technical layers. A customer service, order service, and payment service may sound sensible, but the names alone do not tell us who owns each business decision or how change moves through the system.
Begin with business capabilities and the language used by domain experts. A service should own a coherent set of rules, data, and outcomes. Its boundary should reduce coordination, not merely move method calls onto the network.
A useful test: if two services must be deployed together for most changes, their boundary is probably in the wrong place.
Before splitting a system, document the reason. Independent scaling, separate release cadence, regulatory isolation, team autonomy, and failure containment are strong reasons. A desire to appear modern is not. For many products, a well-structured modular monolith remains the safer starting point and can preserve future extraction paths.
A focused Azure reference architecture
A production platform does not need every Azure service. It needs the smallest set of managed capabilities that satisfy its reliability, security, and operational requirements. One practical baseline looks like this:
Internet
│
Azure Front Door + Web Application Firewall
│
API Management
│
Azure Container Apps
├── Orders API ───── Azure SQL
├── Payments API ─── Azure SQL
└── Worker services
│
Azure Service Bus
Shared platform capabilities
├── Microsoft Entra ID
├── Azure Key Vault
├── Azure Monitor + Application Insights
└── Container RegistryAzure Front Door provides a global entry point, TLS termination, routing, and web application firewall protection. API Management centralizes external API policies, versioning, quotas, and consumer-facing documentation. Keep business logic out of the gateway.
Azure Container Apps is a good default when a team wants container portability, revision-based deployments, and event-driven scaling without operating Kubernetes. Azure Kubernetes Service becomes appropriate when you truly need Kubernetes APIs, deeper network control, specialized workloads, or a platform team capable of owning its complexity. App Service remains an excellent fit for straightforward HTTP applications.
Choose communication by business need
Synchronous calls for immediate answers
Use HTTP or gRPC when a caller cannot proceed without an immediate response. Keep call chains short. Every synchronous hop increases latency and creates another failure mode. Apply explicit timeouts, limited retries with jitter, and circuit breakers. Never allow an unbounded retry policy to amplify an outage.
Asynchronous messages for durable progress
Use Azure Service Bus when work can continue independently, must survive a temporary outage, or should reach multiple consumers. Commands express an intent for one owner. Events state that something already happened. Treat both as durable contracts.
Consumers must be idempotent because messages can be delivered more than once. Store a message identifier or design operations so replaying them produces the same result. Use dead-letter queues as an operational workflow, not as a forgotten message cemetery. Alert on growth and provide a safe process for inspection and replay.
Give each service authority over its data
A service boundary is incomplete if other services can update its tables. Each service should own its schema and expose changes through an API or event. Separate databases are the strongest enforcement, although separately owned schemas can be a transitional step.
Cross-service transactions require a different mental model. Instead of trying to recreate a distributed ACID transaction, model the workflow as a sequence of local transactions with compensating actions. A payment may be authorized, an order confirmed, and inventory reserved in separate steps. The workflow should make intermediate states visible and recoverable.
The transactional outbox pattern closes a common reliability gap: update business data and write the outgoing event to an outbox in the same local transaction, then publish it asynchronously. This prevents a process crash from leaving the database updated but the event missing.
Make identity the perimeter
Authenticate users and workloads with Microsoft Entra ID. Prefer managed identities between Azure resources so applications do not carry long-lived credentials. Put the remaining secrets and certificates in Key Vault, apply least-privilege access, and rotate them.
Validate authorization inside the service that owns the protected operation. A gateway can reject obviously invalid requests, but it cannot replace domain-level authorization. Carry correlation identifiers, not sensitive personal data, through logs and messages. Encrypt traffic in transit and data at rest, then use private networking where the threat model and compliance obligations justify its operational cost.
Security also includes the delivery chain. Pin and scan dependencies, generate a software bill of materials, scan container images, sign artifacts where appropriate, and prevent critical findings from quietly entering production.
Design observability before the first incident
Centralized logs are useful, but they are not enough. A production platform needs three connected signals: metrics tell you that something is wrong, traces show where time and failure moved through the system, and structured logs provide local detail.
Instrument .NET services with OpenTelemetry and propagate trace context across HTTP calls and Service Bus messages. Send telemetry to Azure Monitor and Application Insights. Standardize a small set of fields such as service name, environment, trace ID, operation, outcome, and dependency. Avoid high-cardinality labels that make metrics costly and difficult to query.
Measure what users experience. Availability, latency, error rate, queue age, and business completion rate matter more than CPU alone. Define service-level objectives and connect alerts to the remaining error budget. An alert should be actionable and owned; otherwise it becomes noise.
Make delivery boring and reversible
Build each artifact once, promote the same immutable image through environments, and keep environment-specific configuration outside the image. Provision infrastructure with Bicep or Terraform and review infrastructure changes with the application change.
Use revision-based or blue-green releases for safe rollout. Run automated smoke checks against the new revision before shifting traffic. Database changes need expand-and-contract migrations: add backward-compatible structures first, deploy code that supports both states, migrate data, and remove the old structure only after every consumer has moved.
Every deployment should have a tested rollback or roll-forward path. Feature flags can separate deployment from release, but flags need owners and expiry dates. Otherwise temporary branches become permanent complexity.
A practical production checklist
- Each service boundary maps to a business capability and a clear owner.
- Every dependency has a timeout and an intentional retry policy.
- Message consumers are idempotent and dead-letter queues are monitored.
- Services own their data; cross-service workflows tolerate partial completion.
- Managed identities replace stored cloud credentials wherever possible.
- Traces cross HTTP and messaging boundaries with consistent correlation.
- Dashboards and alerts reflect user-visible reliability objectives.
- Deployments are incremental, observable, and reversible.
- Runbooks name an owner and explain recovery for likely failure modes.
- Architecture decisions record both the choice and its tradeoffs.
Production readiness is not a product feature that can be added at the end. It is the result of explicit boundaries, controlled failure, secure identity, observable behavior, and repeatable delivery. Start with those qualities, then select the Azure services that support them.