Skip to content
Home
Cloud Architecture: Microservices, Event-Driven, Design

Cloud Architecture: Microservices, Event-Driven, Design

Cloud Computing Cloud Computing 9 min read 1713 words Intermediate ExcellentWiki Editorial Team

Introduction

Building applications for the cloud requires a fundamentally different approach than traditional enterprise software. Applications designed for on-premises data centers assume reliable networks, predictable capacity, and static infrastructure. Cloud-native applications assume the opposite — networks are unreliable, capacity is elastic, and infrastructure is ephemeral.

Cloud architecture patterns have emerged through years of real-world experience building distributed systems at scale. These patterns address common challenges — service discovery, fault tolerance, data consistency, and observability — that arise when applications span multiple services, availability zones, and geographic regions. Understanding these patterns is essential for architects and engineers building modern cloud applications.

Twelve-Factor App Principles

The twelve-factor app methodology, developed by engineers at Heroku, provides a set of principles for building cloud-native applications. These principles apply to any application deployed as a service in a cloud environment.

Codebase and Dependencies

Each application maintains a single codebase tracked in version control, deployed to multiple environments — development, staging, production. Dependencies are declared explicitly through package management tools and isolated from the system’s default libraries. This ensures consistency across environments and eliminates “it works on my machine” problems.

Configuration and Backing Services

Configuration that varies between deployments — database URLs, API keys, environment names — is stored in environment variables rather than hardcoded in the application code. Backing services — databases, message queues, caches — are treated as attached resources accessed through URLs or connection strings, enabling switching between local development services and cloud-managed services without code changes.

Build, Release, Run

Build, release, and run are strictly separated stages. The build stage converts code into an executable bundle. The release stage combines the build with environment configuration. The run stage executes the release. This separation enables rolling back to previous releases and promoting releases across environments without rebuilding.

Processes and Statelessness

Application processes are stateless and share nothing. Persistent state must be stored in a backing service — database, object storage, cache — not on local filesystem memory. Stateless processes enable horizontal scaling, because any instance can handle any request, and simplify recovery, because failed instances are replaced without data loss.

Port Binding and Concurrency

Applications are self-contained and export HTTP services by binding to a port. This enables multiple applications to coexist on the same host and simplifies integration with routing layers. The process model scales out by running multiple instances of stateless processes, not by scaling up a single monolithic process.

Disposability and Dev/Prod Parity

Applications maximize robustness with fast startup and graceful shutdown. Processes start quickly to enable rapid scaling and deploy. They shut down gracefully, completing in-flight requests and notifying the platform. Parity between development and production environments — same operating system, same backing services, same deployment method — minimizes environment-specific bugs.

Logs and Admin Processes

Applications treat logs as event streams — unbuffered, time-ordered streams of events that are captured, aggregated, and routed to analysis tools by the execution environment. Admin or maintenance tasks — database migrations, data imports, one-time scripts — run as one-off processes in the same environment as the application, using the same codebase and configuration.

Microservices Architecture

Microservices decompose applications into independently deployable services that communicate over well-defined APIs. Each service owns a specific business capability, runs its own process, and can be developed, deployed, and scaled independently.

Service Decomposition

The primary challenge in microservices is determining service boundaries. Domain-driven design provides a framework for identifying bounded contexts — areas of the business where specific terms, rules, and logic apply. Each bounded context maps to a potential microservice.

Good service boundaries align with business capabilities, are small enough for a single team to own, and change independently of other services. The ordering service handles order placement and management. The inventory service tracks stock levels. The payment service processes transactions. Each service encapsulates its data and exposes functionality through APIs.

API Communication

Services communicate through well-defined APIs, typically REST over HTTP or gRPC for high-performance scenarios. API gateways provide a single entry point for external clients, routing requests to appropriate services, handling authentication, rate limiting, and request transformation.

Service mesh technologies like Istio and Linkerd manage inter-service communication at the infrastructure layer, providing traffic management, security, and observability without code changes. They handle retries, timeouts, circuit breaking, and mutual TLS between services.

Data Management

Each microservice owns its data store. Services never access another service’s database directly. This decentralization prevents tight coupling but introduces data consistency challenges. Event-driven patterns help maintain consistency across services.

The saga pattern manages distributed transactions by breaking them into local transactions with compensating actions. If a transaction spans order creation, payment, and inventory update, each step commits locally. If a later step fails, compensating transactions undo previous steps.

Microservices Challenges

Microservices introduce complexity that monolithic applications avoid. Network latency replaces in-process calls. Distributed transactions are harder than single-database transactions. Testing across service boundaries requires integration test suites. Debugging distributed failures requires sophisticated observability. Not every application needs microservices — start monolithic and extract services as needed.

Event-Driven Architecture

Event-driven architecture decouples components through asynchronous event communication. Services publish events when something significant happens — an order placed, a payment completed, a user registered. Other services subscribe to relevant events and react accordingly.

Event Streaming

Events are published to event streams — ordered, durable, and replayable sequences. Apache Kafka has become the standard event streaming platform, providing high-throughput, fault-tolerant event storage and processing. Cloud alternatives include AWS Kinesis, Azure Event Hubs, and Google Cloud Pub/Sub.

Event streaming enables multiple consumers to process the same event independently. An order-placed event might trigger inventory deduction, payment processing, shipping notification, and analytics updates — all running in separate services without coordination.

Event Sourcing

Event sourcing stores the complete history of state changes as a sequence of events rather than the current state. To determine current state, you replay all events. This provides a complete audit trail, enables temporal queries, and supports rebuilding state from scratch.

CQRS separates read operations from write operations. Commands handle writes, producing events that update the write model. Queries read from a separate read model optimized for specific access patterns. Eventual consistency between write and read models is acceptable because read models update asynchronously from events.

Design Patterns for Cloud Applications

Circuit Breaker

Circuit breakers prevent cascading failures by detecting when downstream services are unhealthy and stopping requests before they time out or fail. When failure rates exceed a threshold, the circuit breaker trips to open state, immediately failing requests without attempting the call. After a timeout, the breaker transitions to half-open state, allowing a limited number of test requests. If they succeed, the breaker closes. If they fail, the breaker reopens.

This pattern prevents thread pool exhaustion, reduces latency for users during partial outages, and gives downstream services time to recover. Implement circuit breakers with libraries like Resilience4j, Hystrix, or as part of a service mesh configuration.

Retry with Exponential Backoff

Transient failures — network timeouts, database deadlocks, service throttling — are common in distributed systems. Retry with exponential backoff handles these failures by retrying failed operations with increasing delays between attempts. Jitter randomization prevents retry storms where all clients retry simultaneously.

Set maximum retry limits to avoid indefinite retries. Use different retry strategies for idempotent operations that can be safely retried and non-idempotent operations that require deduplication.

Bulkhead

Bulkhead isolation limits the impact of failures by partitioning resources into separate pools. If one partition fails, others continue operating. Named after the watertight compartments on ships that prevent flooding from spreading, this pattern applies to thread pools, database connections, and service instances.

A web application with separate thread pools for each downstream service ensures that a slow database does not exhaust threads available for cache lookups. Service instances partitioned by customer tier prevent a high-traffic customer from degrading service for other customers.

Cache-Aside and Write-Through

Caching improves performance and reduces load on backend services. Cache-aside loads data into the cache on first request, with subsequent requests served from cache. The application checks the cache first, loads from the database on miss, and updates the cache. Write-through writes update the cache synchronously with the database, ensuring consistency but adding latency.

Use distributed caches like Redis or Memcached for shared caching across service instances. Set appropriate time-to-live values and implement cache invalidation strategies to prevent stale data.

Strangler Fig

The strangler fig pattern incrementally replaces a monolithic application with microservices. New functionality is built as microservices that integrate with the monolith. Over time, microservices handle more functionality while the monolith handles less. The monolith is eventually decommissioned.

This pattern enables gradual migration without big-bang rewrites. Each microservice extracts a well-defined capability. The monolith continues running during the transition. Traffic gradually shifts from monolith to microservices as functionality is replaced. For more on implementing these patterns, see the Microservices Guide and the Serverless Computing Guide.

FAQ

Should every application use microservices? No. Start with a well-structured monolith. Extract services when the monolith’s complexity exceeds a single team’s ability to manage, or when independent scaling requirements demand separate deployment. Premature microservices add complexity without corresponding benefits.

What is the difference between event-driven architecture and microservices? They are complementary. Microservices define how functionality is decomposed into services. Event-driven architecture defines how those services communicate. Many microservices applications use event-driven communication for asynchronous workflows.

How do I handle distributed transactions? Avoid distributed transactions where possible by designing service boundaries that keep related data in the same service. When transactions must span services, use the saga pattern with compensating actions. Most cloud applications tolerate eventual consistency for non-critical operations.

What is a service mesh and do I need one? A service mesh handles inter-service communication at the infrastructure layer — traffic management, security, and observability. You need one when you have many services and require consistent policies for traffic routing, mTLS, and observability without code changes. Start without one and add when operational complexity justifies it.

How do I test cloud architecture patterns? Test patterns in isolation with unit tests for circuit breakers and retry logic. Use integration tests that deploy services in containers and verify interactions. Use chaos engineering to validate resilience patterns — inject failures into production-like environments and verify the system survives.

Related Articles

Section: Cloud Computing 1713 words 9 min read Intermediate 990 articles in section Report inaccuracy Back to top