Understanding Microservices

Hey! If you love Go and building Go apps as much as I do, let's connect on Twitter or LinkedIn. I talk about this stuff all the time!

Want to learn how to build better Go applications faster and easier? You can.

Check out my course on the Go Standard Library. You can check it out now for free.


Understanding Microservices: Building Blocks of Modern Applications

Introduction

Imagine you’re building a complex application, like a social media platform. This kind of program often needs to handle many different tasks simultaneously – user profiles, posts, comments, likes, shares, messages, and more.

In the past, this meant everything had to be in one giant codebase. But today, we can break down these tasks into smaller, independent “services.” These services communicate with each other and work together to form a complete application.

Think of it like building a house:

  • Traditional Architecture: Building the entire house with one monolithic blueprint.

  • Microservices: Designing the house using separate blueprints for each room (or function). This allows for independent development and deployment of each part, making the process more flexible and efficient.

Why it matters

Service discovery is crucial in a distributed system like a microservice architecture because it helps with:

  • Flexibility: Instead of hard-coding addresses, services can dynamically discover each other, making it easier to add, remove, or scale individual parts of the application.
  • Resilience: If one server goes down, other servers can still communicate and function even though they need to find a new service instance.

Scaling & Service Discovery: A Dynamic Duo

Breaking down an application into smaller services allows you to handle more users and traffic. But how do these services “know” where to find each other? This is the core problem that service discovery solves.

How it works:

Imagine a phonebook for your microservices. When one service needs to find another, it doesn’t need to know the exact address of the other service. It can simply look up the name of the service in the phonebook and get the necessary information to connect with it.

This “phonebook” is called a service registry.

  • Services as independent units: Instead of having all components in a single program, each part is designed as a separate microservice with its own logic and responsibilities.

Best Practices for Efficient & Readable Microservices

While the concept of a “distributed phonebook” for our services is helpful, it’s important to remember that we need to maintain consistency between all these independent units.

Let’s say you have a microservice for handling user logins. You might want this service to be able to talk to different databases depending on the environment (e.g., local database vs. cloud-based database) and to other services like user profile management, post creation, and comment moderation.

  • Don’t hardcode:

Instead of writing each service’s address directly into the code, a better approach is to treat them as abstract entities.

  • Use the “Service Discovery” concept:

This allows the login service to find other services it needs to access (like database addresses) from the “registry” instead of needing to know their specific locations.

Choosing Between Service Discovery & Traditional Addressing:

  • Traditional Server Addressing:

In a traditional application, each client would need to know the exact address of all the databases in order to connect with them. This is impractical for complex systems and can lead to issues when servers move or change.

  • Service Discovery Advantages:
    • Scalability: Imagine you have 100 servers and a “login service” service needs to be able to scale up and down based on the number of users needing it.
    • Flexibility:

Instead of directly knowing the addresses, the “login service” can dynamically discover its dependencies (like the “user profile” database) by looking them up in a registry.

Example: Using a Service Registry for a Simple Microservice Architecture

Let’s imagine you have a simple microservice architecture with two services:

  • UserService: This service handles user registration and authentication, storing user details.
  • CommentService: This service handles the creation and management of comments for the “login” process.

Instead of hardcoding the address of the “CommentService” in your “UserService,” you can use a service registry to dynamically locate it.

Step 1: Define Your Services

First, you need to define how your services will communicate with each other. You can’t just hardcode IP addresses and assume they’ll always be the same because we want our applications to be able to find new instances of “UserService” and “CommentService”.

  • User Authentication:
// Imagine this is a simple code snippet for your UserService
type UserService struct {
  // ...
}
// Implement the communication logic with the registry
func (us *UserService) RegisterUser(username, password string) {
  // This function would be responsible for connecting to and interacting 
  // with the "CommentService" (e.g., for creating a user account).

  // ...
}

type CommentService struct {
  // ...
}
// Imagine this is your simple function to store/retrieve comments
func (cs *CommentService) RegisterUser(name string) {
    // This function assumes the "CommentService" has logic to 
    // interact with the service registry and users can register
    // through the system.
}

Step 2: Implement a Service Discovery Mechanism

There are many ways to approach this, but for simplicity’s sake, let’s imagine we have a simple “service discovery” service that lets you register users with an associated function for handling the “CommentService” (e.g., adding a new comment).

  • Implementing the “discovery” logic:
    • You can use a database to store information about available services and their locations (e.g., database addresses, usernames).
      The “CommentService” service would need a function that handles the registration process.

Step 3: Implement the “UserService”

You’d need to implement functions for user authentication in your “UserService” based on the database implementation.

  • Define the “Auth” function:

This function, called authenticateUser, would be responsible for interacting with the Post and GetComment functions of a service registry (e.g., a simple “login” functionality).

Step 4: Implement the “authentication” logic

// Imagine this is your code to handle the database interaction
func (us *UserService) authenticateUser(username, password string) bool {

  // ... Implement logic to fetch user credentials and compare them with the "GetComment" function's authentication criteria. 

  return true // For demonstration purposes. Replace with real authentication logic
}

// In a real system:
// This would be a more complex example, but the core idea remains the same:
func (us *UserService) authenticate(username, password string) bool {
  // ... Use these credentials to access and interact with "UserService"

  // Example user authentication logic:
  if isValidPassword(username, password) {
    // Authenticate user against the database
  } else {
    return false // Authentication failed.

  }

  // ...
}

Step 5: Use a Service Discovery Service to connect to “CommentService”

Let’s say you have a function for sending a notification to a user, SendNotification, that you want to call when someone creates a new comment. This function would be responsible for interacting with the “CommentService” and adding the user’s information to it.

// ... (Inside your "UserService" code)

// Assuming 'User' is a struct representing a user, 
// we can add a 'user' field to the 'Authenticate' function

func (us *UserService) Authenticate(username string) bool {
    // Check for authentication logic using the `Auth` function
}

Example Function:

Let’s see how a “UserService” might interact with a database to get user information.



Stay up to date on the latest in Coding for AI and Data Science

Intuit Mailchimp