10 Java Microservices Best Practices Every Developer Should Follow ✅

📘 Premium Read: Access my best content on Medium member-only articles — deep dives into Java, Spring Boot, Microservices, backend architecture, interview preparation, career advice, and industry-standard best practices.

✅ Some premium posts are free to read — no account needed. Follow me on Medium to stay updated and support my writing.

🎓 Top 10 Udemy Courses (Huge Discount): Explore My Udemy Courses — Learn through real-time, project-based development.

▶️ Subscribe to My YouTube Channel (172K+ subscribers): Java Guides on YouTube

🧾 Introduction

👋 Hey developers,
 If you’re building Java microservices and want them to run smoothly in production, then this article is for you.

You can break a monolith into 10 microservices in a weekend.
But building scalable, resilient, and secure microservices?
That’s where most Java developers mess up.

They follow tutorials — but skip the real-world best practices.

In this guide, we’ll fix that. You’ll learn 10 essential Java microservices best practices, each explained with the mistake, best practice, and code examples.

 I am a bestseller Udemy Instructor. Check out my top 10 Udemy courses with discountsMy Udemy Courses — Ramesh Fadatare.

Let’s go 👇

1. Database Per Microservice

❌ Mistake

All services share a single database. One schema change breaks the entire system.

✅ Best Practice

Give each microservice its own dedicated database.

💡 Real-World Example

-- Inventory DB
CREATE TABLE stock (
product_id VARCHAR(50) PRIMARY KEY,
quantity INT NOT NULL
);

-- Order DB
CREATE TABLE orders (
id UUID PRIMARY KEY,
product_id VARCHAR(50),
quantity INT
);

Why It Matters

  • Avoids tight coupling
  • Services can be scaled or replaced independently
  • Enables true microservice isolation

✅ 2. Use Event-Driven Communication

❌ Mistake

Synchronous REST calls between services create tight coupling and cascading failures.

✅ Best Practice

Use asynchronous communication with Kafka or RabbitMQ for critical flows.

💡 Real-World Example

  • Order Service publishes an OrderPlaced event
  • Payment Service consumes it and processes payment
  • Inventory Service reserves stock on the same event
// OrderService - Event Publisher
kafkaTemplate.send("orderPlaced", new OrderPlacedEvent(orderId, productId, quantity));

// PaymentService - Event Consumer
@KafkaListener(topics = "orderPlaced")
public void handlePayment(OrderPlacedEvent event) {
paymentService.process(event.orderId());
}

Why It Matters

  • Improves fault tolerance
  • Services work independently
  • Handles spikes better (no waiting on response)

✅ 3. Use DTOs — Never Expose JPA Entities

❌ Mistake

Exposing internal entities like Order directly through your REST APIs.

✅ Best Practice

Use DTOs to clearly define your API contract.

💡 Real-World Example

// Request DTO
public record CreateOrderRequest(String productId, int quantity) {}

// Response DTO
public record OrderResponse(UUID orderId, String status) {}
@PostMapping("/orders")
public ResponseEntity<OrderResponse> placeOrder(@RequestBody CreateOrderRequest request) {
Order order = orderService.placeOrder(request.productId(), request.quantity());
return ResponseEntity.ok(new OrderResponse(order.getId(), order.getStatus()));
}

Why It Matters

  • Protects internal models
  • Prevents leaking sensitive fields
  • Allows APIs to evolve safely

✅ 4. Add Circuit Breakers, Timeouts, and Retries

❌ Mistake

Assuming all external services (like payment or inventory) will always respond.

✅ Best Practice

Wrap remote calls with circuit breakers and configure timeouts and fallbacks.

💡 Real-World Example

If Payment Service is down, return a fallback response or retry.

Code Snippet (using Resilience4j)

@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
public PaymentResponse callPaymentService(PaymentRequest request) {
return paymentClient.pay(request);
}

public PaymentResponse fallbackPayment(PaymentRequest req, Throwable ex) {
return new PaymentResponse("FAILED", "Payment service unavailable");
}

Why It Matters

  • Avoids crashing the entire system
  • Improves availability and graceful degradation
  • Helps recover from temporary failures

✅ 5. Centralized Logging and Monitoring

❌ Mistake

Logging only to the console or log file — no visibility across services.

✅ Best Practice

Use tools like ELK Stack, Prometheus, and Grafana for full observability.

💡 Real-World Example

  • Logs go to Elasticsearch
  • Metrics go to Prometheus
  • Traces show full request path across microservices

Why It Matters

  • You can track bugs and performance issues
  • Helps during outages
  • Enables better alerting and monitoring

✅ 6. Make Event Consumers Idempotent

❌ Mistake

Assuming Kafka or RabbitMQ will deliver events only once.

✅ Best Practice

Make event handlers idempotent — safe to reprocess the same event.

💡 Real-World Example

@KafkaListener(topics = "paymentSucceeded")
public void reserveStock(PaymentSucceededEvent event) {
if (!inventoryRepository.existsByOrderId(event.orderId())) {
inventoryRepository.reduceStock(event.productId(), event.quantity());
}
}

Why It Matters

  • Prevents duplicate processing
  • Keeps data consistent
  • Ensures exactly-once-like behavior

✅ 7. Version Your APIs from Day One

❌ Mistake

Exposing /api/orders and changing it without warning — breaking existing clients.

✅ Best Practice

Use API versioning: /api/v1/orders, /api/v2/orders, etc.

💡 Real-World Example

@GetMapping("/api/v1/orders")
public OrderResponse getV1() {
// old structure
}

@GetMapping("/api/v2/orders")
public OrderDetails getV2() {
// new structure
}

Why It Matters

  • Lets old clients continue working
  • Supports smooth upgrades
  • Avoids breaking production apps

✅ 8. Handle Graceful Shutdowns

❌ Mistake

Killing a service instantly — leaving messages unprocessed or DB connections open.

✅ Best Practice

Implement graceful shutdowns that finish in-flight tasks before exit.

💡 Real-World Example

@PreDestroy
public void shutdown() {
kafkaConsumer.close();
connectionPool.close();
}

Why It Matters

  • Prevents data loss
  • Ensures safe deployments
  • Avoids half-processed requests

✅ 9. Don’t Overuse Shared Libraries

❌ Mistake

Putting core logic, entities, and DB access into a shared “common-lib”.

✅ Best Practice

Limit shared libraries to DTOs or utility code only.

Avoid sharing database models and business logic.

💡 Real-World Example

  • ✅ Share OrderPlacedEvent.java
  • ❌ Don’t share OrderRepository.java

Why It Matters

  • Prevents tight coupling
  • Allows independent evolution
  • Avoids one change breaking everything

✅ 10. Secure by Design

❌ Mistake

Leaving microservices wide open — no auth, no rate limits, no encryption.

✅ Best Practice

Secure services using OAuth2, JWT, role-based access, and rate limiting.

💡 Real-World Example

  • Gateway validates JWT
  • Downstream services check scopes and roles
  • Rate limit per IP using Bucket4j
@Configuration
public class SecurityConfig {
// Use JWT filter here for token validation
}

Why It Matters

  • Protects user data
  • Prevents abuse
  • Meets compliance requirements

Final Recap: Best Practices Table

🎯 Final Thoughts

✅ Microservices are not about splitting code — they’re about splitting ownership, responsibilities, and failures.

✅ Following best practices early saves months of pain later.

Good developers build microservices.
Great developers build microservices that are resilient, observable, scalable, and secure.

Focus on these 10 practices — and your Java microservices will truly shine in real-world production!

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare