← Back to Home

Module 2 - Service Design

Module Overview

Explore service design principles and best practices for building scalable applications.

Service Design Principles

Service design is the process of applying your technical knowledge to build services that solve business problems. It involves selecting the right technologies, balancing requirements, and researching potential solutions.

The Service Design Process

  1. Gather Requirements: Collect the functional and non-functional requirements for the problem you aim to solve.
  2. Break Down Components: Divide the system into smaller, manageable subsystems and services.
  3. Compare Alternatives: Evaluate multiple potential solutions by weighing their pros and cons.
  4. Document and Review: Create a design document and get feedback through design reviews.

Practical Service Design Principles

When comparing technologies like AWS Lambda vs. other AWS Compute options (ECS, EC2), consider factors such as operational maintenance, flexibility, and specific requirements like statelessness, execution time limits, and memory constraints.

Service Design Patterns

Throughout software development history, design patterns have evolved to solve common problems. These patterns implement specific pieces of logic and serve as building blocks for your designs.

Adapter Pattern

The adapter pattern improves compatibility by bridging incompatible interfaces to work together. It translates between different interfaces or data formats.

Example: Translating between different ID formats (e.g., converting SKUs to UUIDs) when integrating with other services.

public class InventoryServiceAdapter {
  private InventoryService inventoryService;

  public InventoryServiceAdapter(InventoryService inventoryService) {
    this.inventoryService = inventoryService;
  }

  public MovieInfo getMovieInfo(UUID movieId) {
    // Convert our UUID to the SKU format the inventory service expects
    String sku = convertToSku(movieId);
    // Call the inventory service
    InventoryItem item = inventoryService.getItemBySku(sku);
    // Convert the response to our internal format
    return convertToMovieInfo(item);
  }

  private String convertToSku(UUID id) {
    // Conversion logic
  }

  private MovieInfo convertToMovieInfo(InventoryItem item) {
    // Conversion logic
  }
}

Facade Pattern

The facade pattern provides a simplified interface to a complex system. It makes a system easier to use by hiding its complexities.

Example: Creating a read-only client for external users while maintaining full CRUD functionality internally.

public class MovieServiceReadOnlyFacade {
  private MovieService movieService;

  public MovieServiceReadOnlyFacade(MovieService movieService) {
    this.movieService = movieService;
  }

  // Only expose read operations
  public MovieInfo getMovie(String id) {
    return movieService.getMovie(id);
  }

  public List<MovieInfo> searchMovies(String query) {
    return movieService.searchMovies(query);
  }

  // No create, update, or delete methods exposed
}

Proxy Pattern

The proxy pattern creates a substitute or placeholder for another object to control access to it. It's commonly used to isolate changes in external services.

Example: Creating a proxy for third-party services to isolate your system from their changes.

public class MovieInfoServiceProxy {
  private ThirdPartyMovieInfoService thirdPartyService;
  private Cache cache;

  public MovieInfoServiceProxy(ThirdPartyMovieInfoService service, Cache cache) {
    this.thirdPartyService = service;
    this.cache = cache;
  }

  public MovieDetails getMovieDetails(String movieId) {
    // Check cache first
    MovieDetails cachedDetails = cache.get(movieId);
    if (cachedDetails != null) {
      return cachedDetails;
    }

    // Not in cache, call the third-party service
    try {
      MovieDetails details = thirdPartyService.getDetails(movieId);
      cache.put(movieId, details);
      return details;
    } catch (Exception e) {
      // Handle service errors gracefully
      log.error("Third-party service error", e);
      return getFallbackDetails(movieId);
    }
  }
}

Notifier Pattern

The notifier pattern provides a way for components to register for and receive notifications about events. It decouples event occurrence from notification handling.

Example: A service that notifies multiple systems when a movie's inventory status changes.

Aggregator Pattern

The aggregator pattern combines data from multiple sources into a single response. It simplifies client logic by centralizing data retrieval and combination.

Example: A service that combines movie details, pricing, availability, and user reviews from different systems into a single comprehensive view.

Remember that while these patterns are useful, they should only be applied when needed to solve a specific problem. Overengineering by applying too many patterns can lead to unnecessary complexity.

Learning Objectives

Resources