logo

Spring Boot Common Module for Microservices: Best Practices

One of the most common failure patterns in microservice ecosystems has nothing to do with distributed systems, networking, or scalability.

It begins with copy-paste.

A team creates a User Service and implements a global exception handler. A second service needs similar behavior, so the code is copied. A third service needs localization support, so that code is copied as well. Over time, every service develops its own slightly different implementation of response handling, validation, logging, error formatting, localization, and utility functions.

Initially, this duplication appears harmless.

As the platform grows, it becomes one of the most expensive forms of technical debt.

A bug fix requires updates across multiple repositories. Error responses become inconsistent. Localization behaves differently across services. Engineering standards begin to drift.

The Common Module exists to prevent this outcome.

Rather than copying infrastructure code across services, shared concerns are centralized into a versioned library that can evolve independently while enforcing consistency across the platform.

In this article, we’ll examine how a centralized Common Module reduces duplication, improves maintainability, and establishes engineering standards across a Spring Boot microservices ecosystem.

By Bharat Chaudhari July 16, 2026

The Hidden Cost of Copy-Paste Architecture

Microservice architectures naturally encourage service independence.

Unfortunately, many teams misinterpret independence as duplication.

Consider a platform containing:

  • User Service
  • Payment Service
  • Notification Service
  • Authorization Service

Each service requires:

  • Exception handling
  • Validation
  • Response formatting
  • Localization
  • Utility functions

If every service implements these concerns independently, small inconsistencies begin to appear.

One service return:

				
					{  
  "message": "User not found"  
}  
				
			
Another return:
				
					{  
  "error": "User not found"  
}  
				
			
A third return:
				
					{  
  "status": 404,  
  "details": "User not found"  
}  
				
			

From an API consumer’s perspective, the platform no longer behaves consistently.

The Common Module eliminates this divergence by providing shared infrastructure capabilities through a centralized dependency.

Why we built a common module

The objective was never simply to reduce code duplication.

The larger goal was architectural governance.

The Common Module provides a mechanism for enforcing platform-wide standards in areas such as:

  • Error handling
  • API response structures
  • Localization
  • Validation
  • Shared DTOs
  • Utility classes

This creates a contract that every service follows.

As new services are added, they inherit these standards automatically rather than reimplementing them from scratch.

This dramatically improves consistency while reducing development effort.

Architecture Diagram

Without Common Module
				
					User Service  
├── Exception Handler  
├── ApiResponse  
├── i18n  
 
Payment Service  
├── Exception Handler  
├── ApiResponse  
├── i18n  
 
Notification Service  
├── Exception Handler  
├── ApiResponse  
├── i18n  
				
			

Problems: Duplication, Drift, Inconsistency

With Common Module

				
					Common Module  
├── Exception Handler  
├── ApiResponse  
├── i18n  
├── Utilities  
        ↓  
User Service  
Payment Service  
Notification Service  
Authorization Service  
				
			

Benefits: Consistency, Governance, Faster Development

What belongs in a common module?

One of the most important architectural questions is determining what should be shared.

The answer is: Infrastructure concerns.

Good candidates include:

  • Exception Handling: Consistent error responses across services.
  • API Response Models: Standardized response contracts.
  • Localization: Shared message resolution and language support.
  • Validation Utilities: Reusable validation logic.
  • Common Constants: Platform-wide constants and configuration helpers.
  • Shared Utility Classes: Date handling, formatting, string manipulation, and helper methods.
  • Distributed Tracing: Centralized Micrometer and Zipkin configurations.

These concerns are cross-cutting and benefit from centralization.

What Should NOT Be Shared?

Many organizations eventually create an oversized common library that becomes more problematic than duplication.

A Common Module should never contain:

  • Business Logic: Business rules belong within the service that owns the domain.
  • Domain Models: User entities should not appear in Payment Services. Payment entities should not appear in Notification Services.
  • Service-Specific APIs: Shared libraries should not expose functionality tightly coupled to individual services.
  • Database Access: Repositories and persistence logic should remain within service boundaries.

The guiding principle is simple: Share infrastructure. Do not share business capabilities.

Architecture & Tech Stack

ComponentDetails
Language & FrameworkJava 21 & Spring Boot 4.0.3
TypeShared Java library (.jar) — NOT a standalone service
PackagingMaven artifact, installed via mvn clean install
Consumersuser-service, auth-service, payment-service, notification-service
Key ComponentsGlobalExceptionHandler, ApiResponse<T>, I18nConfig, Shared DTOs

Standardized API Responses

One of the easiest ways for distributed systems to become inconsistent is through response formatting.

Without standards, every service evolves its own conventions.

The Common Module establishes a single response contract used throughout the platform.

Benefits include:

  • Predictable client integrations
  • Reduced frontend complexity
  • Consistent documentation
  • Easier monitoring and debugging

Because every service returns responses in the same structure, consumers can process responses without special-case handling for individual services.

				
					@Getter @Setter @Builder  
public class ApiResponse<T> {  
 
    @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")  
    private LocalDateTime timestamp;  
    private Integer status;  
    private String message;  
    private T data;  
}  
				
			

Dependencies
To build the API Gateway using Spring Cloud Gateway, Spring WebFlux, and Spring Security, include the following dependencies.

				
					<dependencies> 
    <!-- Spring Cloud Gateway (Reactive) --> 
    <dependency> 
        <groupId>org.springframework.cloud</groupId> 
        <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId> 
    </dependency> 
 
    <!-- Eureka Client --> 
    <dependency> 
        <groupId>org.springframework.cloud</groupId> 
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 
    </dependency> 
 
    <!-- Load Balancer --> 
    <dependency> 
        <groupId>org.springframework.cloud</groupId> 
        <artifactId>spring-cloud-starter-loadbalancer</artifactId> 
    </dependency> 
 
    <!-- Security --> 
    <dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-security</artifactId> 
    </dependency> 
 
    <dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-actuator</artifactId> 
    </dependency> 
 
    <!-- OAuth2 Resource Server --> 
    <dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> 
    </dependency> 
 
    <!-- Test --> 
    <dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-test</artifactId> 
        <scope>test</scope> 
    </dependency> 
</dependencies>
				
			

Consistent Error Handling Across Services

Error handling is often implemented differently in every service.

This creates several problems:

  • Inconsistent error formats
  • Duplicate code
  • Difficult debugging
  • Different localization behavior

The GlobalExceptionHandler centralizes these concerns.

Rather than forcing every service to implement identical exception handling logic, services inherit a common implementation and extend it only when necessary.

This ensures that:

  • Error structures remain consistent
  • Localization behaves identically
  • Logging standards remain aligned

while still allowing service-specific customization.

				
					public class GlobalExceptionHandler  
        extends ResponseEntityExceptionHandler {  
 
    @ExceptionHandler(ResourceNotFoundException.class)  
    public ResponseEntity<Object> handleNotFound(  
            ResourceNotFoundException ex,  
            HttpServletRequest request) {  
 
        ErrorResponse error = buildErrorResponse(  
                ex, HttpStatus.NOT_FOUND, request);  
 
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);  
    }  
 
    @ExceptionHandler(Exception.class)  
    public ResponseEntity<Object> handleAll(  
            Exception ex, HttpServletRequest request) {  
        ErrorResponse error = buildErrorResponse(  
                ex, HttpStatus.INTERNAL_SERVER_ERROR, request);  
        return new ResponseEntity<>(error,  
                HttpStatus.INTERNAL_SERVER_ERROR);  
    }  
}  
				
			

How to Use GlobalExceptionHandler in Other Services

In this setup, the GlobalExceptionHandler is intentionally not annotated with @RestControllerAdvice. This gives you better flexibility and control. Instead of letting Spring auto-detect it, we design it to be reused through inheritance.

Add the Dependency to Your Service
Next, include this shared module in the service where you want to use it.

				
					<dependency>  
        <groupId>com.common-module</groupId>  
        <artifactId>common-module</artifactId>  
        <version>0.0.1-SNAPSHOT</version>  
        <scope>compile</scope>  
</dependency>  
				
			

Extend the Base Handler
Now, inside your service, create a new class that extends the shared handler and annotate it.

				
					@RestControllerAdvice  
public class ServiceExceptionHandler extends GlobalExceptionHandler {  
}
				
			

Customize When Needed
If your service has its own exceptions, you can easily extend or override the behavior.

				
					@ExceptionHandler(CustomServiceException.class)  
public ResponseEntity<Object> handleCustom(  
        CustomServiceException ex,  
        HttpServletRequest request) {  
   
    ErrorResponse error = buildErrorResponse(  
            ex, HttpStatus.BAD_REQUEST, request);  
   
    return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);  
}  
				
			

Localization as a platform capability

Many applications introduce localization only after international users arrive.

Retrofitting localization into an existing platform is often painful because messages become scattered across controllers, services, and exception classes.

The Common Module treats localization as a first-class capability from the beginning.

By centralizing locale resolution and message source configuration, every service automatically gains multilingual support without implementing its own localization infrastructure.

This allows teams to add additional languages with minimal application changes while maintaining consistent user experiences across the platform.

				
					@Configuration  
public class I18nConfig implements WebMvcConfigurer {  
 
    private static final List<Locale> SUPPORTED_LOCALES =  
            Arrays.asList(new Locale("en"), new Locale("fr"));  
 
    @Bean  
    public LocaleResolver localeResolver() {  
        return new AcceptHeaderLocaleResolver() {  
            @Override  
            public Locale resolveLocale(HttpServletRequest request) {  
                String headerLang = request.getHeader("Accept-Language");  
                return headerLang == null || headerLang.isEmpty()  
                        ? Locale.getDefault()  
                        : Locale.lookup(  
                            Locale.LanguageRange.parse(headerLang),  
                            SUPPORTED_LOCALES);  
            }  
        };  
    }  
}
				
			

Centralized Distributed Tracing

A major benefit of the Common Module is centralizing distributed tracing. By including `micrometer-tracing-bridge-brave` and `zipkin-reporter-brave`, we ensure all microservices emit consistent trace headers and spans.

To bypass missing or complex Spring Boot autoconfigurations, the Common Module includes `TracingManualConfig`:

				
					@Configuration 
public class TracingManualConfig { 
    @Bean 

@ConditionalOnMissingBean 
    public DefaultTracingObservationHandler tracingObservationHandler(Tracer tracer) { 
        return new DefaultTracingObservationHandler(tracer); 
    } 
    // ... configures Zipkin sender, Brave Tracing, and Micrometer Tracer 
} 
				
			

Any service depending on the Common Module will automatically start reporting distributed traces to Zipkin without writing custom configurations in every service. 

Versioning Strategy

Shared libraries introduce different operational challenges upgrades.

Every service consuming in the Common Module must remain compatible with new releases.

For this reason:

  • Breaking changes should be minimized.
  • Semantic versioning should be followed.
  • Backward compatibility should be preserved where possible.

Typical versioning approach:

  • Major Version: Breaking changes.
  • Minor Version: New features.
  • Patch Version: Bug fixes.

This allows services to upgrade predictably while avoiding unexpected runtime behavior.

Production Considerations

Dependency Governance
Every shared library becomes a platform of dependency.

Changes must be reviewed carefully because they can impact multiple services simultaneously.

Backward Compatibility
Avoid introducing changes that require all services to upgrade immediately.

Incremental adoption is far safer.

Release Management
Treat shared libraries with the same rigor as production services.

Changes should undergo:

  • Code review
  • Testing
  • Version tagging
  • Release documentation

Documentation
Shared modules are only valuable when engineers understand how to use them.

Every reusable component should include:

  • Usage examples
  • Extension patterns
  • Migration notes

Architectural Benefits

The Common Module provides benefits beyond code reuse.

  • Consistency: All services behave similarly.
  • Faster Development: New services start with established standards.
  • Easier Maintenance: Bug fixes occur once instead of multiple times.
  • Improved Governance: Architectural standards become enforceable rather than optional.
  • Better Developer Experience: Teams focus on business capabilities rather than rebuilding infrastructure repeatedly.

Conclusion

The Common Module is rarely the most visible component in a microservices platform, but it is often one of the most important.

Without shared standards, microservice ecosystems gradually drift toward inconsistency. Error handling differs between services, response structures evolve independently, and infrastructure concerns are repeatedly reimplemented.

By centralizing cross-cutting concerns into a versioned shared library, teams establish a foundation of consistency without sacrificing service independence.

The result is not merely less code duplication. The result is a platform that behaves predictably, scales more effectively, and remains maintainable as the number of services and engineering teams grows.

In many ways, the Common Module acts as the architectural backbone of the platform, ensuring that every service speaks the same language even as they evolve independently.

Source Code
GitHub: https://github.com/inexture-solutions/springboot-microservices/tree/main/common-module

FAQs

Why not simply copy code between services?
Copying infrastructure code appears faster initially but creates long-term maintenance challenges. Bug fixes, enhancements, and standards of enforcement become increasingly difficult as duplication grows.

Can a Common Module become too large?
Yes.

Many organizations eventually create “God Libraries” containing unrelated functionality. Shared modules should focus on infrastructure concerns rather than business capabilities.

Should domain entities be shared?

Generally, not.

Domain ownership should remain within individual services.

How do services adopt new Common Module features?

Through versioned releases and dependency upgrades.

This allows teams to adopt changes incrementally rather than requiring platform-wide updates.

Is a Common Module required in every microservices architecture?

Not necessarily.

Smaller systems may not benefit immediately. As the number of services grows, however, shared infrastructure becomes increasingly valuable.

Bharat Chaudhari is a Java Backend Engineer with 5+ years of experience building scalable microservices using Spring Boot. He specializes in distributed systems, event-driven architecture, Spring Security, Kafka, PostgreSQL, Redis, Docker, and Kubernetes, with a passion for building production-ready backend platforms.

Delivering Engineering Excellence Across Global Markets

  • India
  • USA
  • UAE
  • Europe
  • Singapore
  • Australia

Need Help Building Enterprise Spring Boot Microservices?

We specialize in designing secure, scalable, and event-driven microservices using Spring Boot, Kafka, PostgreSQL, Redis, OAuth2/JWT, Docker, Kubernetes, and cloud-native best practices for enterprise applications.