How to Build and Orchestrate Microservices

Sebastian Petrus
7 min readSep 6, 2024

--

Microservices architecture has revolutionized the way we build and deploy applications. This guide will provide a detailed, step-by-step approach to building microservices and orchestrating them effectively. We’ll cover everything from initial design to deployment and maintenance, with practical examples and best practices.

Part 1: Designing Microservices

Before we continue, let’s talk about something that we all face during development: API Testing with Postman for your Development Team.

Yeah, I’ve heard of it as well, Postman is getting worse year by year, but, you are working as a team and you need some collaboration tools for your development process, right? So you paid Postman Enterprise for…. $49/month.

Now I am telling you: You Don’t Have to:

APIDog: You Get Everything from Postman Paid Version, But CHEAPER

That’s right, APIDog gives you all the features that comes with Postman paid version, at a fraction of the cost. Migration has been so easily that you only need to click a few buttons, and APIDog will do everything for you.

APIDog has a comprehensive, easy to use GUI that makes you spend no time to get started working (If you have migrated from Postman). It’s elegant, collaborate, easy to use, with Dark Mode too!

APIDog makes you very easy to migrate from Postman with No Learning Curve

Want a Good Alternative to Postman? APIDog is definitely worth a shot. But if you are the Tech Lead of a Dev Team that really want to dump Postman for something Better, and Cheaper, Check out APIDog!

1.1 Understanding Microservices Architecture

Microservices architecture is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API.

Key characteristics:

  • Independently deployable
  • Loosely coupled
  • Organized around business capabilities
  • Owned by small teams
How to Build and Orchestrate Microservices

1.2 Identifying Service Boundaries

1.2.1 Domain-Driven Design (DDD)

Use DDD to identify bounded contexts, which will form the basis of your microservices.

Example: For an e-commerce application, bounded contexts might include:

  • Product Catalog
  • Order Management
  • User Management
  • Inventory
  • Shipping

1.2.2 Single Responsibility Principle

Each microservice should have a single responsibility and reason to change.

Example: The Order Management service handles:

  • Creating orders
  • Updating order status
  • Retrieving order information

It does not handle inventory management or user authentication.

1.3 Designing Service Interfaces

1.3.1 API Design

Design RESTful APIs for synchronous communication:

GET /orders/{orderId}
POST /orders
PUT /orders/{orderId}
DELETE /orders/{orderId}

1.3.2 Event-Driven Design

For asynchronous communication, design events:

{
"event": "OrderCreated",
"data": {
"orderId": "12345",
"userId": "user789",
"items": [
{"productId": "prod456", "quantity": 2}
]
}
}

1.4 Data Management

1.4.1 Database per Service

Each service should have its own database to ensure loose coupling.

Example:

  • Order Service: MongoDB
  • Product Catalog: PostgreSQL
  • User Service: MySQL

1.4.2 Data Consistency

Implement eventual consistency using event-driven architecture:

  1. Order Service creates an order
  2. Publishes “OrderCreated” event
  3. Inventory Service consumes event and updates stock

Part 2: Implementing Microservices

2.1 Choosing Technology Stack

Select appropriate technologies for each service based on its requirements:

Example:

  • Order Service: Node.js with Express, MongoDB
  • Product Catalog: Java with Spring Boot, PostgreSQL
  • User Service: Python with Flask, MySQL

2.2 Implementing a Microservice

Let’s implement the Order Service as an example:

2.2.1 Project Structure

order-service/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── orderservice/
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── repository/
│ │ │ ├── model/
│ │ │ └── OrderServiceApplication.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
├── pom.xml
└── Dockerfile

2.2.2 Implementing the API

@RestController
@RequestMapping("/orders")
public class OrderController {
    @Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
Order createdOrder = orderService.createOrder(order);
return new ResponseEntity<>(createdOrder, HttpStatus.CREATED);
}
@GetMapping("/{orderId}")
public ResponseEntity<Order> getOrder(@PathVariable String orderId) {
Order order = orderService.getOrder(orderId);
return new ResponseEntity<>(order, HttpStatus.OK);
}
}

2.2.3 Implementing Business Logic

@Service
public class OrderService {
    @Autowired
private OrderRepository orderRepository;
@Autowired
private KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;
public Order createOrder(Order order) {
Order savedOrder = orderRepository.save(order);
// Publish OrderCreated event
kafkaTemplate.send("order-created", new OrderCreatedEvent(savedOrder));
return savedOrder;
}
public Order getOrder(String orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
}

2.2.4 Implementing Data Access

@Repository
public interface OrderRepository extends MongoRepository<Order, String> {
}

2.2.5 Implementing Event Publishing

@Configuration
public class KafkaConfig {
    @Bean
public KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ProducerFactory<String, OrderCreatedEvent> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(config);
}
}

2.3 Testing Microservices

2.3.1 Unit Testing

@RunWith(MockitoJUnitRunner.class)
public class OrderServiceTest {
    @Mock
private OrderRepository orderRepository;
@Mock
private KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;
@InjectMocks
private OrderService orderService;
@Test
public void testCreateOrder() {
Order order = new Order();
when(orderRepository.save(any(Order.class))).thenReturn(order);
Order result = orderService.createOrder(order); verify(orderRepository).save(order);
verify(kafkaTemplate).send(eq("order-created"), any(OrderCreatedEvent.class));
assertEquals(order, result);
}
}

2.3.2 Integration Testing

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class OrderControllerIntegrationTest {
    @Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testCreateOrder() throws Exception {
Order order = new Order();
order.setUserId("user123");
mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(order)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.userId").value("user123"));
}
}

Part 3: Containerization and Deployment

3.1 Containerizing Microservices

3.1.1 Writing a Dockerfile

FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/order-service-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

3.1.2 Building and Pushing Docker Image

docker build -t order-service:v1 .
docker tag order-service:v1 your-registry/order-service:v1
docker push your-registry/order-service:v1

3.2 Kubernetes Deployment

3.2.1 Deployment YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: your-registry/order-service:v1
ports:
- containerPort: 8080
env:
- name: SPRING_DATA_MONGODB_URI
value: mongodb://mongodb-service:27017/orders

3.2.2 Service YAML

apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer

3.2.3 Deploying to Kubernetes

kubectl apply -f order-service-deployment.yaml
kubectl apply -f order-service-service.yaml

Part 4: Service Orchestration

4.1 Service Discovery

Use Kubernetes DNS for service discovery. Services can communicate using <service-name>.<namespace>.svc.cluster.local.

4.2 API Gateway

Implement an API Gateway using Kong or Ambassador:

apiVersion: getambassador.io/v2
kind: Mapping
metadata:
name: order-service
spec:
prefix: /orders/
service: order-service

4.3 Service Mesh

Implement Istio for advanced traffic management, security, and observability:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- route:
- destination:
host: order-service
subset: v1
weight: 90
- destination:
host: order-service
subset: v2
weight: 10

4.4 Centralized Logging

Implement the ELK stack (Elasticsearch, Logstash, Kibana) for centralized logging:

  1. Deploy Elasticsearch and Kibana
  2. Configure Filebeat as a DaemonSet to collect logs
  3. Use Logstash to process logs before sending to Elasticsearch

4.5 Monitoring and Alerting

Implement Prometheus and Grafana for monitoring:

  1. Deploy Prometheus Operator
  2. Create ServiceMonitor resources for each microservice
  3. Set up Grafana dashboards
  4. Configure AlertManager for alerting

Part 5: Scaling and Optimization

5.1 Horizontal Pod Autoscaling

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: order-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
targetAverageUtilization: 50

5.2 Database Scaling

  1. Implement read replicas for read-heavy services
  2. Use database sharding for write-heavy services
  3. Implement caching using Redis or Memcached

5.3 Asynchronous Processing

Use message queues (RabbitMQ, Apache Kafka) for asynchronous processing:

  1. Deploy RabbitMQ or Kafka cluster
  2. Implement message producers and consumers in services
  3. Use for tasks like sending emails, generating reports, etc.

Part 6: Security

6.1 Authentication and Authorization

Implement OAuth2 and OpenID Connect using Keycloak:

  1. Deploy Keycloak
  2. Configure realms, clients, and roles
  3. Integrate microservices with Keycloak for authentication and authorization

6.2 Network Policies

Implement Kubernetes Network Policies to control traffic between services:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: order-service-policy
spec:
podSelector:
matchLabels:
app: order-service
ingress:
- from:
- podSelector:
matchLabels:
app: api-gateway
ports:
- protocol: TCP
port: 8080

6.3 Secrets Management

Use Kubernetes Secrets for sensitive information:

apiVersion: v1
kind: Secret
metadata:
name: order-service-secrets
type: Opaque
data:
database-password: base64encodedpassword

Part 7: Continuous Integration and Deployment (CI/CD)

7.1 CI/CD Pipeline

Implement a CI/CD pipeline using Jenkins or GitLab CI:

  1. Build and test on every commit
  2. Build Docker image on successful tests
  3. Push image to registry
  4. Update Kubernetes deployment

Example Jenkins Pipeline:

pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
}
stage('Build Docker Image') {
steps {
sh 'docker build -t order-service:${BUILD_NUMBER} .'
}
}
stage('Push Docker Image') {
steps {
sh 'docker push your-registry/order-service:${BUILD_NUMBER}'
}
}
stage('Deploy to Kubernetes') {
steps {
sh 'kubectl set image deployment/order-service order-service=your-registry/order-service:${BUILD_NUMBER}'
}
}
}
}

7.2 Canary Deployments

Implement canary deployments using Istio:

  1. Deploy new version alongside old version
  2. Gradually increase traffic to new version
  3. Monitor for errors or performance issues
  4. Rollback if issues detected, otherwise complete rollout

Conclusion

Building and orchestrating microservices is a complex but rewarding process. This guide has covered the key aspects, from design and implementation to deployment and optimization. Remember that microservices architecture is not a one-size-fits-all solution, and it’s important to carefully consider whether it’s the right approach for your specific use case. Continuous learning and improvement are key to success in the world of microservices.

--

--

Sebastian Petrus
Sebastian Petrus

Written by Sebastian Petrus

Asist Prof @U of Waterloo, AI/ML, e/acc

No responses yet