Back to Microservice Course

Microservice Course

Microservices are small, loosely coupled services that can fail independently from each other.

When a microservice fails, only the single capability it owns should become unavailable, while the rest of the system remains operational.

Principles of Microservices

  • Each microservice should own its data — services should not share databases directly.
  • Avoid unnecessary coupling between services and software components.
  • Independence and autonomy are more important than code reusability.
  • Each microservice should be responsible for a single business capability (Single Responsibility at the service level).
  • Services should prefer loose coupling through events/messages over direct synchronous calls where feasible, though direct synchronous (REST/gRPC) communication is also legitimate and common depending on latency and consistency requirements.

Each microservice should be able to provide a unique, well-bounded business context, and should ideally own its own datastore (database-per-service pattern) so its schema and data model can evolve independently.

Generally implemented as a REST service over HTTP with technology-agnostic APIs, though gRPC is increasingly common for internal service-to-service communication where performance, strong contracts (via Protobuf), and bidirectional streaming matter.

Microservices are

  • REST (or gRPC for internal communication)
  • Small, independently deployable units
  • Cloud-native by design

Challenges of Microservices

  • Bounded Context — defining the right boundaries for each service is the hardest design problem.
  • Configuration Management — managing configuration across many services and environments without drift.
  • Dynamic Scale Up and Scale Down — services need to register themselves and discover others dynamically (addressed by service registries like Eureka, Consul, or Kubernetes-native DNS).
  • Visibility / Distributed Tracing — a single user request may span dozens of services; tracing it end-to-end requires distributed tracing (Zipkin/Sleuth, Jaeger, OpenTelemetry).
  • Fault Tolerance — a failing downstream service must not cascade failures to callers (addressed by circuit breakers like Resilience4j or Hystrix — note: Netflix Hystrix is in maintenance mode since 2018 and the current recommended replacement is Resilience4j).

Spring Cloud addresses these challenges:

Challenge | Spring Cloud Component (current recommendation) -----------------------------------|------------------------------------------------------------------- Service registry & discovery | Netflix Eureka or HashiCorp Consul Centralized configuration | Spring Cloud Config (backed by Git) Distributed tracing | Spring Cloud Sleuth + Zipkin, or Micrometer Tracing + OpenTelemetry Fault tolerance / circuit breaker | Resilience4j (Hystrix is deprecated) Client-side load balancing | Spring Cloud LoadBalancer (Ribbon is deprecated) API gateway | Spring Cloud Gateway (Zuul 1 is in maintenance)

What is Bounded Context?

A Bounded Context is a DDD (Domain-Driven Design) term. It defines an explicit boundary within which a particular domain model is defined and applicable. Inside that boundary, all terms, rules, and models are consistent and unambiguous. Outside the boundary, the same words may mean different things.

Example: In a hospital system, "Patient" means something slightly different in the Emergency Ward context (triaged by severity, tracked by arrival time) than in Billing (tracked by insurance, payment history). These are two different bounded contexts, each with their own model of "Patient," rather than one shared model that tries to serve both concerns.

Bounded Context is arguably the most important concept to get right in a microservices design. Getting the boundaries wrong — either too fine-grained (nanoservices: too much network overhead and operational complexity) or too coarse-grained (distributed monolith: services that are always deployed together and always call each other synchronously) — is the most common cause of painful microservices adoptions.

Is it a good idea for Microservices to share a common database?

No. Sharing a database between multiple microservices tightly couples them at the data layer:

  • One service can read or write another service's tables, breaking encapsulation.
  • Schema changes in one service can break another service without any visible API contract change.
  • Services can't be scaled, deployed, or failed independently if they share a database.
  • It defeats the purpose of Bounded Context.

The Database per Service pattern is the standard recommendation — each service owns its own datastore, and data is shared between services only through APIs or events, never by direct database access.

For scenarios where you need to query data that spans multiple services (reporting, dashboards), the CQRS + Event Sourcing pattern or a dedicated read-model service is the right approach, rather than a shared database.

What is the preferred type of communication between Microservices? Synchronous or Asynchronous?

  • Synchronous (REST/gRPC): A blocking call where the caller waits for a response before continuing. Provides immediate feedback on success or failure and is simpler to implement and debug. Appropriate for real-time, user-facing operations where the result is needed immediately.
  • Asynchronous (event/message-driven): The caller publishes an event or sends a message and moves on without waiting. The receiver processes it independently. Better for operations where eventual consistency is acceptable, for high-throughput event-driven workflows, and for decoupling services that don't need to know each other's availability.

The widely stated claim that "Synchronous Microservices are not loosely coupled" is an oversimplification. Synchronous REST services are loosely coupled in terms of technology (any client can call a REST API), but they are temporally coupled — the caller and callee must both be available at the same time. Asynchronous messaging removes this temporal coupling (callers can fire events even if consumers are temporarily down), but adds complexity around eventual consistency, message ordering, and retry/dead-letter handling. In real enterprise systems, both styles coexist: synchronous for reads and immediate commands, asynchronous for workflows, events, and cross-domain side effects.

SOA vs Microservices

screenshot of the app

Service-Oriented Architecture (SOA) and Microservices both decompose applications into services, but differ significantly in philosophy and implementation:

| | SOA | Microservices | |---|---|---| | Communication | SOAP over HTTP/JMS; relies on ESB | Lightweight REST/gRPC; no central bus required | | Scope | Enterprise-wide service sharing and reuse | Team-owned, independently deployable services | | Data | Often shared databases | Database per service | | Governance | Centralized (ESB, central registry) | Decentralized | | Size | Coarse-grained services | Fine-grained, single-capability services | | Deployment | Often shared containers/app servers | Independent deployments, container-native |

The key distinction is that SOA's reliance on a heavyweight central ESB (Enterprise Service Bus) creates a bottleneck and single point of failure. Microservices replace the ESB with dumb pipes (HTTP, message queues) and put the smarts in the services themselves.

What is the difference between Orchestration and Choreography in Microservices architecture?

  • Orchestration: A central orchestrator service controls the workflow — it calls other services in sequence, handles responses, and decides what to do next. Easier to visualize and trace but introduces a coupling point (the orchestrator knows about and depends on all the other services). Synchronous calls are common. Example: Saga pattern with a central saga orchestrator.
  • Choreography: Each service reacts to events published by other services and publishes its own events in response. No central controller — the workflow emerges from the collective reactions of services. More loosely coupled and resilient, but the overall flow is harder to visualize since it's distributed across many service logs. Example: Saga pattern with choreography (each service publishes and subscribes to domain events).

Neither is universally better. Orchestration is often simpler to start with; choreography scales better and is more resilient in large, complex domains.

Can we create Microservices as State Machines?

Yes. A microservice that manages an entity with a lifecycle (Order, Booking, Loan Application) is naturally modelled as a state machine. State transitions are triggered by commands (user actions) or events (from other services). This maps well onto event-sourcing, where the state machine's transition history is the event log, and the current state is derived by replaying events.

12 Factor App

screenshot of the app

The 12-factor app methodology was developed by Adam Wiggins, co-founder of Heroku, to describe best practices for building software-as-a-service applications that are portable, resilient, and cloud-native. It predates the current Kubernetes era but its principles remain highly relevant.

12 factors for building cloud-native apps:

  1. Codebase: One codebase per service, tracked in version control. Many deployments (dev, staging, prod) from one codebase, with environment differences handled through config (factor 3).
  2. Dependencies: All dependencies explicitly declared (e.g., Maven pom.xml, Gradle build.gradle) — never assume a system-wide library exists. Build should be fully reproducible with pinned versions.
  3. Config: Everything that varies between deployments (dev, staging, prod) is stored in environment variables, never in the source code. This includes DB connection strings, external service URLs, credentials.
  4. Backing services: Treat databases, caches, message queues, SMTP servers as attached resources accessible via URL/config. A local MySQL and a managed RDS instance should be interchangeable by changing config alone.
  5. Build, release, run: Strictly separate the build stage (compile, package), release stage (combine build with config), and run stage (execute in the environment). A release is immutable — to change code or config, create a new release.
  6. Processes: Services are stateless and share-nothing. Any state that needs to persist across requests lives in a backing service (database, cache, object store), not in service memory. This is what enables horizontal scaling.
  7. Port binding: The service is self-contained and exports its own HTTP endpoint via a port it binds to — no dependency on an external web server. Spring Boot's embedded Tomcat/Jetty/Undertow is a direct implementation of this factor.
  8. Concurrency: Scale by adding more instances (horizontal/process-based scaling), not by making a single instance larger (vertical/thread-based scaling). Services should be designed so multiple identical instances can run concurrently behind a load balancer.
  9. Disposability: Services start fast and shut down gracefully. They handle SIGTERM cleanly — completing in-flight requests, releasing connections, then exiting. This is what enables fast scaling, rolling deployments, and resilience to node failures.
  10. Dev/prod parity: Keep dev, staging, and prod as similar as possible — same services, same versions, as small a time gap as possible between writing code and deploying it. Docker and container-based local development (Docker Compose) exist largely to close this gap.
  11. Logs: Treat logs as event streams. The app writes to stdout; log aggregation (shipping to Splunk, ELK, Datadog, etc.) is the platform's concern, not the application's. Never write to log files inside the container.
  12. Admin processes: One-off administrative tasks (database migrations, data cleanup scripts) run in the same environment as the app, using the same codebase and config, and are tracked in version control. Flyway and Liquibase are the standard tools for this in Java/Spring apps.

Beyond the original 12, the cloud-native community has effectively added a 13th factor worth knowing: API-first — defining your API contract (OpenAPI/AsyncAPI spec) before writing implementation code, enabling consumer-driven contract testing (Pact), parallel team development, and clear service boundaries.

Cloud Service Models

screenshot of the app

With cloud computing, service vendors provide three foundational models:

  • IaaS (Infrastructure as a Service): Provides raw compute, storage, and networking. You provision VMs and manage the OS, middleware, runtime, and applications yourself. Examples: AWS EC2, Azure VMs, GCP Compute Engine.
  • PaaS (Platform as a Service): Provides a managed platform — OS, runtime, and middleware are handled by the provider. You focus on application code and data. Examples: AWS Elastic Beanstalk, Google App Engine, Heroku.
  • SaaS (Software as a Service): A fully managed application delivered over the internet. No infrastructure or platform management. Examples: Salesforce, Gmail, Slack, GitHub.

For microservices, a fourth model is now critical:

  • CaaS (Container as a Service): Provides managed container orchestration infrastructure. You package services as containers; the platform manages scheduling, scaling, health, and networking. Examples: Kubernetes on EKS/AKE/GKE, AWS ECS, Azure Container Apps. This is where most modern microservices deployments land.

screenshot of the app