WL
Java Full Stack Developer
Wassim Lagnaoui

Lesson 19: Introduction to Microservices

Discover microservices architecture fundamentals: understanding distributed systems, design principles, service discovery, and the challenges of building scalable microservices applications.

Introduction

As applications grow in complexity and scale, traditional monolithic architectures can become limitations rather than foundations. Microservices represent a fundamental shift from building single large applications to creating ecosystems of small, focused services that work together to deliver complex functionality. This architectural approach enables teams to develop, deploy, and scale different parts of an application independently, leading to greater agility and resilience. However, microservices also introduce new challenges around service coordination, data consistency, and distributed system complexity. This lesson introduces you to the fundamental concepts of microservices architecture, showing how this approach enables massive scalability while presenting new challenges that modern tools like Spring Cloud help solve. You'll understand when to choose microservices over monoliths, learn key design principles, and explore the tools and patterns that make distributed systems manageable.


Understanding Microservices

Definition

  • Microservices architecture structures applications as collections of small, independent services that communicate over well-defined APIs.
  • Each microservice is responsible for a specific business capability, can be developed and deployed independently, and typically owns its data storage.
  • This architectural style enables teams to work autonomously, choose appropriate technologies for each service, and scale different parts of the system independently based on demand.
  • Microservices promote organizational scalability and technological diversity while requiring sophisticated coordination and operational practices.

Analogy

Microservices architecture is like transitioning from a traditional department store to a modern shopping mall with specialized boutique shops. In a department store (monolith), everything is under one roof with shared infrastructure, centralized management, and uniform policies, but changes require coordinating across the entire store, and if one section has problems, it can affect the whole operation. A shopping mall (microservices) has many independent stores, each specializing in what they do best - a bookstore, electronics shop, clothing boutique, and food court. Each store has its own management, inventory systems, operating hours, and customer service approach. They can renovate, change suppliers, or adjust pricing independently without affecting other stores. Customers can visit just the stores they need, and the mall can add new shops or remove failing ones without disrupting the entire complex. However, the mall needs shared infrastructure like parking, security, and directory systems, plus coordination for things like holiday hours and promotional events. This is more complex to manage than a single store, but it provides much more flexibility and resilience.

Examples

Microservice structure:

// Main application class for a microservice
// Each microservice has its own main class and runs independently
@SpringBootApplication
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);  // Bootstrap this microservice
    }
}

Service-specific controller:

// Controller that only handles user-related operations
// Microservice follows single responsibility principle
@RestController
@RequestMapping("/users")
public class UserController {
    // Only handles user-related operations - no orders, payments, etc.
    // Each microservice focuses on one business domain
}

Independent database:

# Each microservice has its own database
# user-service connects to user-specific database
spring.datasource.url=jdbc:mysql://user-db:3306/users
spring.application.name=user-service  # Service identifier for discovery

Service communication:

// Feign client for inter-service communication
// Declarative way to call other microservices via HTTP
@FeignClient(name = "order-service")  // Target service name for service discovery
public interface OrderServiceClient {
    @GetMapping("/orders/user/{userId}")
    List getUserOrders(@PathVariable Long userId);  // Call order-service API
}

Monolith vs Microservices

Definition

  • Monolithic architecture builds applications as single deployable units where all components are interconnected and interdependent, while microservices architecture decomposes applications into small, independent services.
  • Monoliths are simpler to develop, test, and deploy initially, but become harder to scale and modify as they grow.
  • Microservices offer greater flexibility and scalability but introduce complexity in service coordination, data consistency, and operational overhead.
  • The choice depends on team size, scalability requirements, and organizational maturity.

Analogy

The difference between monoliths and microservices is like comparing a Swiss Army knife to a professional toolbox. A Swiss Army knife (monolith) has everything you need in one compact, integrated unit - knife, scissors, screwdriver, and bottle opener all folded into a single tool. It's perfect for simple tasks, easy to carry, and you never lose pieces. However, each tool is smaller and less specialized than dedicated tools, and if one part breaks, the whole knife might be unusable. A professional toolbox (microservices) contains many specialized tools - each designed for specific tasks and potentially made by different manufacturers. You can replace individual tools, add new ones as needed, and use the best tool for each job. However, you need to organize the toolbox, might need multiple tools for complex tasks, and it requires more space and planning to transport. For simple home repairs, the Swiss Army knife is perfect, but for professional construction work, you need the specialized toolbox despite its complexity.

Examples

Monolithic application structure:

// Traditional monolithic project structure
// All functionality packaged in single deployable unit
├── controllers/     // All controllers in one application
├── services/        // All business logic together
├── repositories/    // All data access in one place
└── models/          // All domain models together

Microservices structure:

// Microservices project structure - separate applications
// Each service is independently deployable
user-service/        // Handles user management only
order-service/       // Handles order processing only
payment-service/     // Handles payment processing only
notification-service/ // Handles notifications only

Monolith deployment:

# Single JAR deployment for monolithic application
# One command deploys entire application
java -jar monolith-app.jar

Microservices deployment:

# Multiple service deployment using containers
# Each service deployed and scaled independently
docker-compose up              # Deploys multiple services with Docker
kubectl apply -f microservices.yaml  # Kubernetes deployment

Microservice Design Principles

Definition

  • Microservice design follows key principles: single responsibility (each service does one thing well), autonomous teams (services owned by independent teams), decentralized governance (teams choose their own technologies), fault isolation (failures don't cascade), and evolutionary design (services can evolve independently).
  • These principles ensure that microservices deliver their promised benefits of scalability, flexibility, and team autonomy while avoiding the pitfalls of distributed system complexity.

Analogy

Microservice design principles are like the rules for organizing an effective food truck festival. Each truck (service) specializes in one type of cuisine and operates independently with its own menu, staff, and cooking equipment (single responsibility). Each truck owner makes their own decisions about ingredients, pricing, and operating hours without needing approval from festival organizers (decentralized governance). If one truck breaks down or runs out of food, customers can still eat at other trucks (fault isolation). Truck owners can experiment with new recipes, upgrade equipment, or change their concept without affecting other trucks (evolutionary design). The festival provides shared infrastructure like electricity and waste management, but each truck maintains its independence. This creates a diverse, resilient food scene where innovation thrives, popular concepts can expand, and poor performers can be replaced without disrupting the entire festival.

Examples

Single responsibility service:

// Payment service that only handles payment processing
// Follows single responsibility principle - does one thing well
@RestController
public class PaymentController {
    // Only handles payment processing, nothing else
    @PostMapping("/process")
    public PaymentResult processPayment(@RequestBody PaymentRequest request) {
        return paymentService.process(request);  // Focused on payment logic only
    }
}

Independent data storage:

# Each service owns its data - database per service pattern
# Services cannot directly access other services' databases
user-service.datasource.url=jdbc:mysql://user-db/users      # User service database
order-service.datasource.url=jdbc:postgres://order-db/orders # Order service database

Technology diversity:

// Different services can use different technologies
// Teams choose the best tool for their specific domain
// User service in Java - good for business logic
// Notification service in Node.js - good for real-time features
// Analytics service in Python - good for data processing
// Each team chooses the best technology for their needs

Fault isolation:

// Circuit breaker pattern prevents cascade failures
// If recommendation service fails, it doesn't break the whole system
@HystrixCommand(fallbackMethod = "fallbackRecommendation")
public List getRecommendations(Long userId) {
    return recommendationService.getRecommendations(userId);  // May fail
}

// Fallback method provides default behavior when service fails
public List fallbackRecommendation(Long userId) {
    return Collections.emptyList();  // Safe default when service is down
}

Service Discovery

Definition

  • Service discovery is the mechanism by which services find and communicate with each other in a microservices architecture.
  • Since services can be deployed on different servers and ports, and instances can start, stop, or move dynamically, services need a way to locate each other automatically.
  • Service discovery systems maintain a registry of available services and their locations, enabling services to find dependencies without hardcoded addresses.
  • This supports dynamic scaling, fault tolerance, and simplified configuration management.

Analogy

Service discovery is like the directory system in a large corporate campus with multiple buildings and departments that frequently reorganize. Instead of memorizing that the IT department is in Building 3, Floor 2, Room 205, you consult the dynamic directory system that always knows current locations. When the IT department moves to a new building or adds satellite offices, the directory updates automatically. Employees don't need to know physical addresses - they just look up "IT Support" and get directed to the right place. The directory system also knows if a department is temporarily unavailable (like during a move) and can redirect requests to backup locations. This allows the organization to reorganize, expand, or relocate departments without disrupting daily operations, because everyone always knows how to find the services they need through the central directory system.

Examples

Eureka service registration:

# Service registers itself with Eureka discovery server
# Other services can find this service by name
spring.application.name=user-service  # Service identifier
eureka.client.service-url.defaultZone=http://eureka-server:8761/eureka  # Discovery server URL

Service discovery client:

// Programmatically discover other services
// Get list of available instances for a service
@Autowired
private DiscoveryClient discoveryClient;

// Find all instances of order-service
List instances = discoveryClient.getInstances("order-service");

Load balanced service call:

// RestTemplate with automatic load balancing
// @LoadBalanced enables client-side load balancing
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();  // Automatically load balances requests
}

Dynamic service lookup:

// Call service by name instead of hardcoded URL
// Service discovery resolves "order-service" to actual instance
String response = restTemplate.getForObject(
    "http://order-service/orders/{id}",  // Service name, not IP address
    String.class,
    orderId
);

Distributed Challenges

Definition

  • Microservices introduce distributed system challenges that don't exist in monolithic applications: network latency and failures, data consistency across services, distributed transactions, service versioning, monitoring and debugging across multiple services, and the complexity of testing distributed workflows.
  • These challenges require new tools, patterns, and practices like circuit breakers, distributed tracing, eventual consistency, and comprehensive logging to build reliable distributed systems.

Analogy

Distributed system challenges are like the complexities that arise when transitioning from a single-location restaurant to a restaurant chain across multiple cities. In a single restaurant, if the kitchen runs out of ingredients, they can quickly check the walk-in cooler or send someone to the local market. Communication is instant, inventory is visible, and coordination is simple. With a restaurant chain, each location has its own inventory, staff, and local conditions. If one location runs out of a key ingredient, they can't just walk to another kitchen - they need to coordinate deliveries, check availability across locations, and potentially modify menus independently. A power outage in one city doesn't affect other locations, but it complicates coordination. Customer orders might involve multiple locations (like catering from several branches), requiring careful orchestration. The chain needs sophisticated systems for inventory management, quality consistency, staff training, and performance monitoring across all locations. While this distributed approach enables serving more customers and resilience against local problems, it requires much more sophisticated management and coordination systems than a single restaurant.

Examples

Network failure handling:

// Retry mechanism for handling transient network failures
// Automatically retries failed calls up to 3 times
@Retryable(value = {ConnectException.class}, maxAttempts = 3)
public String callExternalService() {
    return externalServiceClient.getData();  // May fail due to network issues
}

Circuit breaker pattern:

// Circuit breaker prevents cascade failures in distributed systems
// Falls back to default response when service is consistently failing
@HystrixCommand(fallbackMethod = "defaultResponse")
public String riskyServiceCall() {
    return externalService.call();  // May be slow or failing
}

// Fallback method provides alternative when circuit is open
public String defaultResponse() {
    return "Service temporarily unavailable";  // Safe default response
}

Distributed tracing:

// Add tracing to track requests across multiple services
// Helps debug issues in distributed systems
@NewSpan("user-lookup")  // Create new span for tracing
public User findUser(@SpanTag("userId") Long id) {
    return userRepository.findById(id);  // This operation will be traced
}

Eventual consistency:

// Order placed immediately, inventory updated asynchronously
// Demonstrates eventual consistency in distributed systems
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
    // Inventory update happens later, not immediately
    inventoryService.reserveAsync(event.getItems());  // Async processing
}

Spring Cloud Introduction

Definition

  • Spring Cloud is a collection of tools and frameworks that simplify building distributed systems and microservices.
  • It provides solutions for common distributed system patterns like service discovery (Eureka), circuit breakers (Hystrix), API gateways (Gateway), distributed configuration (Config Server), and distributed tracing (Sleuth).
  • Spring Cloud integrates seamlessly with Spring Boot, providing production-ready solutions for the challenges of microservices architecture while maintaining the simplicity and conventions that make Spring Boot popular.

Analogy

Spring Cloud is like a comprehensive urban planning and infrastructure toolkit for building modern smart cities. Just as a city needs electricity grids, water systems, telecommunications, transportation networks, and emergency services to function properly, microservices need service discovery, communication protocols, monitoring systems, and coordination mechanisms. Spring Cloud provides pre-built, tested infrastructure components - like having standardized power grids, water treatment plants, and communication networks that you can deploy and connect rather than building from scratch. The toolkit includes blueprints for common city services: traffic management systems (API gateways), emergency response coordination (circuit breakers), city-wide communication networks (service discovery), and centralized utilities management (configuration servers). This allows city planners (developers) to focus on designing neighborhoods and buildings (business services) rather than figuring out how to distribute electricity or manage water pressure throughout the entire city.

Examples

Spring Cloud dependencies:



<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <!-- Automatically configures service registration and discovery -->
</dependency>

Eureka server setup:

// Creating a service discovery server with Eureka
// Other microservices register with this server
@SpringBootApplication
@EnableEurekaServer  // Enables Eureka server functionality
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);  // Start discovery server
    }
}

Config server:

// Centralized configuration management for microservices
// All services can get their configuration from this server
@SpringBootApplication
@EnableConfigServer  // Enables configuration server
public class ConfigServerApplication {
    // Centralized configuration management for all microservices
}

API Gateway routing:

# Spring Cloud Gateway configuration
# Routes requests to appropriate microservices
spring:
  cloud:
    gateway:
      routes:
      - id: user-service           # Route identifier
        uri: lb://user-service     # Load balanced URI to user-service
        predicates:
        - Path=/users/**           # Route all /users/** requests to user-service

Summary

You've now explored the fundamental concepts of microservices architecture and understand both its benefits and challenges. Microservices offer powerful advantages for large-scale applications through independent development, deployment, and scaling, but they introduce new complexities around service coordination, data consistency, and distributed system management. You've learned key design principles like single responsibility, autonomous teams, and fault isolation that help microservices deliver their promised benefits. Service discovery enables dynamic service location and communication, while Spring Cloud provides a comprehensive toolkit for addressing distributed system challenges with proven patterns and production-ready solutions. Understanding these concepts prepares you for building modern, scalable applications that can grow with your organization's needs. In the next lesson, you'll dive deeper into microservices communication patterns and see how services coordinate to deliver complex business functionality across distributed systems.

Programming Challenge

Challenge: Simple Microservice Setup

Task: Create two basic microservices to demonstrate fundamental microservices concepts.

Requirements:

  1. Create User Service:
    • Create a UserServiceApplication with basic user management endpoints
    • Add a UserController with GET and POST endpoints
    • Configure service to run on port 8081
  2. Create Order Service:
    • Create an OrderServiceApplication with basic order management
    • Add an OrderController with order creation endpoint
    • Configure service to run on port 8082
  3. Add service communication:
    • Use RestTemplate or WebClient to call User Service from Order Service
    • Demonstrate inter-service communication

Learning Goals: Practice creating independent microservices, understand service separation, and implement basic inter-service communication patterns.

Quiz: Introduction to Microservices

Test your understanding of microservices architecture fundamentals.

1. What is the primary characteristic of microservices architecture?

2. What is a major advantage of monolithic architecture over microservices?

3. Which principle ensures that microservices can evolve independently?

4. What is the purpose of service discovery in microservices?

5. Which Spring Cloud component provides service discovery capabilities?

6. What is a circuit breaker pattern used for in distributed systems?

7. What challenge does eventual consistency address in microservices?

8. What is the main benefit of using Spring Cloud in microservices architecture?

9. What is distributed tracing primarily used for?

10. In an interview, how would you explain when to choose microservices over a monolith?