SOFTWARE DEVELOPMENT
Microservices in practice – How to avoid common pitfalls when implementing architecture in Java

This article is part of a series on software development prepared by Sebastian Pieróg.
Microservices in Practice – Introduction
“I had a problem that I solved with a regex… Now I have two problems.”
Almost every developer knows this saying. Regex is powerful, well-known, and widely used – but it can also cause plenty of headaches.
The same goes for microservices. We usually start with a large monolith – slow to start, difficult to scale, and prone to occasional bugs, but still predictable and functional. Then someone at an architecture meeting proposes: “Let’s go microservices – they’re small, fast, simple, and modern.” Inevitably, someone else asks: “Why change something that works?” The answer is usually: “Because this will be better.”
And just like with regex – in theory and on slides, everything looks perfect. Soon, however, we discover that instead of one problem, we now have several or even dozens of new ones we hadn’t considered before: configuration, monitoring, testing, network failures.
The Mini-Monolith – The Most Common Architectural Mistake
One of the most persistent myths is that simply breaking a monolith into several smaller services is enough. Too often, this split is overly conservative, resulting in a few smaller – but still complex – services that each handle multiple business domains.
Such a service may still take a long time to start, have tangled dependencies, and require significant effort to test. It can quickly grow faster than the other microservices, turning into a bottleneck. This also makes teamwork harder, as multiple developers must modify the same large codebase. Without clear domain boundaries, business logic from different areas starts to mix together.
DDD and Bounded Context in Service Design
To avoid this, services should be designed according to the Single Responsibility Principle – and this understanding must be shared by everyone, from business stakeholders to developers. In practice, this means applying Domain-Driven Design (DDD) and defining bounded contexts. This ensures that each microservice owns a clearly defined part of the business domain, rather than an arbitrary set of features.
Strangler Pattern and Refactoring
If a mini-monolith is already in place, the best approach is gradual refactoring using the Strangler Pattern. This pattern allows you to extract functionality step by step without rewriting the entire service at once, minimizing risk, delays, and team overhead.
Communication Between Microservices – Avoiding Pitfalls
It’s common for one development team to own one or several services and define its own coding and architecture practices. In a monolith, communication is just a method call – we share data and have full control over the flow. In microservices, each service is a standalone application, possibly running in a different environment, with its own database, lifecycle, and communication method.
Lack of a unified communication standard can lead to chaos. Choosing the wrong standard can introduce latency or failures. Testing integrations and tracing requests across multiple services becomes harder. The failure of a single service can cascade and take down the whole system.
REST, gRPC, and Event-Driven Architecture
To avoid these issues, you should establish a consistent communication style before development starts.
- Use REST for simple CRUD operations
- Use gRPC for high-performance, low-latency communication
- Use event-driven communication (e.g., Kafka, RabbitMQ) for asynchronous workflows
Each service should have a well-defined and versioned API – tools like Spring Cloud Contract can help. Implement resilience patterns such as Circuit Breaker, Retry, Timeout, and Bulkhead. For monitoring and tracing, adopt tools like OpenTelemetry, Jaeger, or Zipkin.
Security in a Microservices Architecture
Cybersecurity is critical in any system, but microservices increase the attack surface. In a monolith, you secure one application; in a microservices environment, each service can be a potential entry point.
Every service must be protected, function reliably, and communicate securely with others. Inconsistent security practices across teams lead to vulnerabilities. Poorly designed APIs may leak sensitive data, while misconfigured services can expose credentials or database passwords.
OAuth2, TLS, and the Principle of Least Privilege
To prevent this, use a centralized authentication and authorization system, such as OAuth2 with Keycloak, to provide unified access control. Always use secure protocols like TLS/HTTPS. Store secrets and tokens securely using Kubernetes Secrets, Spring Cloud Config with encryption, or HashiCorp Vault.
Apply the Principle of Least Privilege – every service and every user should have access only to the resources strictly necessary to perform their function.
Configuration and Microservice Management
Microservices introduce a large number of independent services, each with its own configuration. This can quickly spiral into chaos. If each service has its own configuration files and environment variables, maintaining consistency becomes difficult. Inconsistent library versions or misconfigurations can lead to outages.
Centralized Configuration and Data Encryption
The solution is centralized configuration management, e.g., Spring Cloud Config, allowing you to manage all microservice settings from a single source and even change them at runtime without restarts.
Use encryption for sensitive data and store it securely in Vault or Kubernetes Secrets. Version your configurations and document dependencies carefully. Centralization, encryption, and monitoring dramatically reduce errors and simplify maintenance.
Monitoring and Logging in Microservices
Microservices often run on separate hosts or in containers, each with its own logs and clock. Analyzing a single request across multiple services can be nearly impossible without the right tooling.
ELK, Prometheus, and OpenTelemetry in Practice
Introduce a Request ID to trace data flow across the entire system. Use centralized logging and monitoring solutions such as the ELK Stack (Elasticsearch, Logstash, Kibana) or Loki + Grafana.
Adopt distributed tracing tools like OpenTelemetry or Zipkin to follow requests through multiple services. For metrics, use Prometheus with Grafana dashboards. Standardize log formatting and log levels. In Spring Boot applications, Spring Boot Actuator helps with health checks and metrics, and Logback integrates well with ELK.
Testing Microservices – Contracts and Integration
Every developer knows the importance of unit testing – but in microservices, contract and integration tests are crucial. Unlike monoliths, microservices run independently, with separate APIs and data.
Contract tests define expectations between a service and its clients, ensuring backward compatibility. Integration tests validate how services work together in an environment close to production.
Contract and Integration Tests in CI/CD
Incorporate automated contract and integration tests into your CI/CD pipeline, executed on every build. This prevents breaking changes from reaching production and significantly reduces maintenance costs.
Conclusion – A Conscious Approach to Microservices
Microservices are not a silver bullet. They offer flexibility, scalability, and independent development but introduce challenges in communication, configuration, security, monitoring, and testing.
It’s easy to follow the hype, but a thoughtful, business-driven approach to microservice design is key to building stable, scalable, and maintainable systems.
Author: Sebastian Pieróg, Senior Fullstack Java Developer, ALTEN Polska