The Case for Splitting AI Directives: A Three-Tiered Git Subtree Approach to Agentic Coding

If you’re running AI coding agents like Junie, Claude, or similar tools across multiple repositories, you’ve probably hit the same wall we did: how do you keep agent instructions consistent across projects without either duplicating everything or losing the ability to customize per-project? Here’s how we solved it using layered git subtrees, and why the structure has paid off.

The Problem: One Size Doesn’t Fit All

AI coding agents work best when given clear, explicit directives — how to commit code, which tools to prefer, how to structure new modules, what standards to follow. But an organization rarely has one set of rules that applies everywhere:

  • Some directives are genuinely universal and worth open-sourcing (e.g., “sign your commits”, “prefer IDE-native tooling over raw shell commands”).
  • Some directives encode proprietary business logic or internal architecture decisions that should never leave the company’s private infrastructure.
  • Some directives only make sense for one specific repository — its build profile, its issue tracker, its deployment quirks.

Cramming all of this into a single flat AGENTS.md file either leaks IP into open-source repository, or forces every project to carry irrelevant boilerplate, or both.

The Structure: Three Tiers, One Entry Point

Our solution is a lightweight AGENTS.md at the root of each repository that acts purely as a navigation index — it doesn’t contain directives itself, just pointers, read in priority order:

  1. Public AI Configurationdocs/public-ai-skills/Readme.md
  2. Private AI Configurationdocs/private-ai-skills/Readme.md
  3. Project AI Configurationdocs/project-ai-skills/Readme.md
  4. Followed by the standard README.md, and finally CHANGELOG.md for historical context only.
# Agent Navigation and Best Practices

This document provides directives to assist AI agents in navigating and working within the `blah` project.

1. Public AI Configuration: [docs/public-ai-skills/Readme.md](docs/public-ai-skills/Readme.md)
2. Private AI Configuration: [docs/private-ai-skills/Readme.md](docs/private-ai-skills/Readme.md)
3. Project AI Configuration: [docs/project-ai-skills/Readme.md](docs/project-ai-skills/Readme.md)
4. Project Readme: [README.md](README.md)
5. Changelog: only read this if looking for historical changes: [CHANGELOG.md](CHANGELOG.md)

Each tier lives in its own folder, and two of those folders — the public and private ones — are actually git subtrees, meaning they’re synced from independent, standalone repositories rather than being native content of the project.

Tier 1 — Public AI Skills (open source, shared broadly)

This layer holds directives that are safe and genuinely useful outside the company: preferring IDE tooling for file operations, commit-signing conventions, guidance on maintaining changelogs, and general Maven/archetype standards. Because it’s pulled from and pushed to a public GitHub repository (LimeMojito/ai-skills) via git subtree pull/push, any project that adopts this subtree automatically benefits when the shared skill set improves — and improvements we make while working in one codebase can flow back out to benefit every other project using the same subtree.

See out public AI skills in Github here.

Tier 2 — Private AI Skills (closed source, org-wide)

This layer holds directives that are proprietary — internal archetype preferences, closed-source skill documentation, and anything that shouldn’t be visible outside the organization. It’s synced from a private GitHub repository (LimeMojito/private-ai-skills) using the identical subtree mechanism. Crucially, it declares itself as taking precedence over the public tier: if a private directive contradicts a public one, private wins. This lets the organization override or refine open-source defaults without forking or duplicating the public skill files.

Tier 3 — Project AI Skills (local, repository-specific)

This layer is native to the project source itself — not a subtree. It captures things that only make sense in this one codebase: which Maven profile to build with, where the parent POM comes from, git/issue-number conventions specific to this repository’s GitHub project board, and pointers to deeper architectural docs. It sits on top of the other two tiers, adding concrete specifics without needing to touch the shared subtrees at all.

Why Split It Up At All?

1. Correct information hygiene. Proprietary business rules never accidentally end up in a public repository, because they physically live in a different, private repository. Conversely, generally useful conventions aren’t held hostage inside a closed-source project where other teams (or the open-source community) can’t benefit from them.

2. Reuse without duplication. Because the public and private tiers are git subtrees rather than copy-pasted files, an improvement made while working in any project using that subtree can be pushed upstream and pulled into every other project that shares it. Fix a directive once, and every repository benefits after the next subtree pull — no manual copy-paste, no drift between slightly different versions of “how to sign a commit.”

3. Clear precedence resolves conflicts predictably. With three tiers and an explicit priority order (private overrides public, project adds specifics on top of both), an agent never has to guess which rule wins when two directives disagree. The AGENTS.md index and the “Primary Directive” callout in the private tier make the resolution order unambiguous.

4. Right-sized context per project. A small open-source utility repo doesn’t need to carry a company’s entire internal tooling philosophy, and a proprietary project doesn’t need to expose its build secrets in a structure meant for public reuse. Each project only pulls in the tiers relevant to it, and the project-specific tier stays lean because it only needs to state what’s different from the shared layers.

5. Independent evolution and versioning. Because each tier is its own git history (two of them literally separate repositories), they can evolve at different speeds. Public conventions might change slowly and deliberately (since external consumers depend on them); private conventions can iterate quickly to match internal process changes; and project-specific tweaks can be made instantly without any subtree ceremony at all.

Conclusion

Treating AI agent directives as a layered, subtree-based configuration — rather than a single monolithic instructions file — mirrors a pattern we already trust in software architecture: separate concerns, share what’s common, override what’s specific, and keep proprietary information behind the right boundary. As more teams lean on coding agents day-to-day, this kind of structured directive management stops being a nice-to-have and starts being essential infrastructure, exactly like a shared library or a common CI pipeline.

Converting Java Exceptions to HTTP Responses

When building serverless applications on AWS using Java and Spring Cloud Function, managing exceptions and returning meaningful HTTP responses to AWS API Gateway is crucial. The ApiGatewayResponseDecorator in the lambda-utilities library simplifies this process for your Java based lambdas. The decorator converts exceptions into a valid AWS API Gateway HTTP event response, preserving HTTP status codes.

See our code in oss-maven-standards on GitHub.

AWS Deployment Diagram

The decorator ensures that your Lambda functions remain clean of boilerplate HTTP response mapping, allowing you to focus on business logic while maintaining a consistent error handling contract with API Gateway.

graph TD
    Client[Client/Browser] -->|HTTPS| API[AWS API Gateway]
    API -->|Proxy| Lambda[AWS Lambda Function]
    subgraph Lambda Execution
        Decorator[ApiGatewayResponseDecorator]
        Function[Your Business Logic]
        Mapper[ApiGatewayExceptionMapper]
    end
    Lambda --> Decorator
    Decorator -->|Call| Function
    Function -.->|Throws Exception| Decorator
    Decorator -->|Maps| Mapper

How it works

The ApiGatewayResponseDecorator acts as a function wrapper that handles the entire request/response lifecycle for AWS API Gateway events.

Sequence Diagram

sequenceDiagram
    participant Client
    participant APIGateway
    participant LambdaFunction
    participant Decorator
    participant ExceptionMapper

    Client->>APIGateway: Request
    APIGateway->>LambdaFunction: Invoke
    LambdaFunction->>Decorator: process
    Decorator->>Decorator: execute function
    alt Success
        Decorator-->>LambdaFunction: 200 OK
    else Failure
        Decorator->>ExceptionMapper: map(Exception)
        ExceptionMapper-->>Decorator: HttpStatus
        Decorator-->>LambdaFunction: HttpStatus + Error JSON
    end
    LambdaFunction-->>APIGateway: Response
    APIGateway-->>Client: Response

Exception Conversion Process

  1. Request Reception: The decorator intercepts the input (an APIGatewayV2HTTPEvent).
  2. Execution: It delegates the actual work to the next function in the pipeline.
  3. Exception Catching: If an exception occurs, the decorator catches it and uses an ApiGatewayExceptionMapper to determine the correct HTTP status code.
  4. Response Construction: It creates an APIGatewayV2HTTPResponse with the correct status code, a content-type header, and a JSON body containing error details.

Error Management Logic

The ApiGatewayExceptionMapper provides the logic for determining the HTTP status code from an exception:

  • @ResponseStatus: If the exception is annotated with @ResponseStatus, the decorator respects the provided code or value as the HTTP status code.
  • ConstraintViolationException: Automatically maps to 400 Bad Request.
  • AccessDeniedException: Automatically maps to 403 Forbidden.
  • Default: Any other exception defaults to 500 Internal Server Error.

Example

Full example in development-test/jar-lambda-poc in oss-maven-standards repo. These jars are available on Central for use with maven.

Include the following dependency for your spring-cloud function:

<dependency>
   <groupId>com.limemojito.oss.standards.aws</groupId>
   <artifactId>lambda-utilities</artifactId>
   <version>18.0.x</version>
 </dependency>

In your spring cloud function code, you can decorate your function using the decorator factory as below:

public Function<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> hello(ApiGatewayResponseDecoratorFactory decoratorFactory) {
        log.info("Initialized Decorator Function");
        return decoratorFactory.create((apiEvent) -> {
            log.info("Received {}", apiEvent);
            return "world";
        });
    }

Supercharging Monorepo CI: How Lime Mojito Uses Incremental Maven Builds

In a large monorepo, a single change to one module shouldn’t force a full rebuild of the entire project. For the Lime Mojito OSS Development Standard Build, we use the gitflow-incremental-builder (GIB) Maven plugin to keep CI cycles fast, efficient, and reliable.

The GitHub gitflow-incremental-builder plugin compares your feature branch to a base branch using git to determine which maven modules require a build. The plugin also marks dependent modules so they will be rebuilt as well.

How it works in the pom.xml

The project enables incremental builds by configuring the gitflow-incremental-builder as a Maven extension. This is managed in the root pom.xml in three key areas:

  1. Plugin Management: The plugin is defined in the <pluginManagement> section (version 4.6.0), ensuring version consistency across the monorepo.
  2. The CI Profile: A specific ci profile is used to activate the plugin. By setting <extensions>true</extensions>, the plugin hooks into the Maven lifecycle to intercept the build process and calculate which modules actually need to run based on Git history.
  3. IDE Compatibility: A clever profile named disable-gib-in-intellij is automatically activated when the idea.version property is present. This disables GIB (gib.disable=true) when working inside IntelliJ IDEA, preventing it from interfering with the IDE’s internal incremental compilation and ensuring a smooth developer experience.

Integration with GitHub Actions

The oss-feature-build.yml workflow implements a sophisticated two-step strategy to leverage incremental builds in CI:

  • Step 1: The Primer (Fast Build)
    The workflow first runs a “Fast Build” (-Pfast-build -Dgib.disable install). This step explicitly disables GIB and skips heavy tasks like tests and Checkstyle. Its purpose is to “prime” the local Maven repository by installing all modules, ensuring that artifacts for unchanged modules are available as dependencies for the next step.
  • Step 2: Targeted Validation (Incremental Build)
    The core CI build runs clean install but uses GIB to limit execution. It dynamically detects the default branch (e.g., master or main) and passes it via -Dgib.referenceBranch. GIB then analyzes the Git diff between the feature branch and the reference branch, selecting only the changed modules—and any downstream modules that depend on them—for testing and building.

Why Incremental Builds for Monorepos?

  1. Reduced Feedback Loops: Developers receive PR feedback in minutes rather than hours, as unrelated modules are skipped entirely.
  2. Resource Efficiency: By avoiding redundant work, the project significantly reduces the consumption of GitHub Actions runner minutes, lowering CI costs.
  3. Automated Dependency Tracking: GIB ensures safety by automatically identifying and building downstream dependents of changed modules, catching potential integration breaks that a simple “changed-files-only” approach would miss.
  4. Scalability: As the monorepo grows from 10 modules to 100, the build time for a single-module change remains constant rather than scaling linearly.

Working example

Lime Mojito OSS Development Standard Build (GitHub)

Announcing Lime MQL Editing 2026.1.0

We are excited to announce the first major release of the Lime MQL Editing plugin for IntelliJ IDEA, version 2026.1.0.  Program trading algorithms in Metaquotes Query Language (MQL) 4 or 5? This release marks a significant milestone as we take over the maintenance and development of the original MQL Idea plugin, providing continued support for the MQL4 and MQL5 programming languages within the JetBrains ecosystem.

A New Chapter: The Lime Mojito Fork

The original plugin by investflow.ru had become unmaintained, and Lime Mojito Pty Ltd has stepped in to ensure that MQL developers can continue to use their favourite IDE with modern features.

Key changes in this transition include:

  • Rebranding & Namespace Migration: The project has been rebranded as Lime MQL Editing. As part of this, the entire codebase has been migrated to the com.limemojito.oss namespace, providing a clean slate for future development.
  • Modernized Build Stack: We have updated the project to use Java 21 and Gradle 9.3. This ensures compatibility with the latest development tools and provides a more robust build environment.
  • IntelliJ IDEA 2025.3+ Compatibility: The plugin has been updated to support the latest versions of IntelliJ IDEA (build 253.30387 and above), taking advantage of recent platform improvements.

Features and Improvements

In this initial release, we focused on stabilizing the core functionality that MQL developers rely on:

  • Comprehensive MQL4/MQL5 Support: Full syntax highlighting, code completion, and navigation for both MQL4 and MQL5.
  • Inlined Documentation: Quick documentation lookup (Ctrl+Q) remains available in both English and Russian, helping you stay in the flow while coding.
  • Improved Project Structure: We’ve cleaned up the repository, removing legacy files and streamlining the plugin’s internal structure.
  • New Plugin Icon: A fresh new look with a dedicated plugin icon.
  • Automated CI/CD: We’ve implemented GitHub Actions for automated builds, signing, and releases, ensuring that every update is verified and secure.

Important Note on Compiler Support

In this version, we have clarified our support for external tools. While the plugin provides excellent editing capabilities, launching the MQL compiler directly from the IDE on Windows and Linux is currently not supported. We recommend using the MetaEditor compiler alongside the IDE for the compilation step while we investigate better integration options for future releases.

Getting Started

If you are currently using a legacy version of the MQLIdea plugin, we recommend the following steps for a smooth transition:

  1. Uninstall any previous versions of the MQLIdea plugin.
  2. Restart IntelliJ IDEA.
  3. Search for Lime MQL Editing in the JetBrains Marketplace and install it.
  4. Restart once more to finalize the installation.

We are committed to maintaining this project as an open-source tool under the GPL3 license. You can follow our progress and report issues on our GitHub repository.

Happy Trading and Coding!

— The Lime Mojito Team

References

Spring Boot 4: Updating Open Source Standards

In late November 2025 Spring Boot 4 was released, built on the new Spring Framework 7. There are a number of API, starter packaging changes and a major api change for JSON parsing to use Jackson 3. Also Amazon Web Services SDK 1 reached end of life so we updated all utilities to use SDK 2. JUnit 4 dependencies were also removed so any older tests were update to JUnit 5 and JAssert.

We left our JVM at 21 and made the 25 upgrade a separate activity.

See OSS Github 16.0.3

Our changes to make version 16 compatible with Spring Boot 4 included:

1. Version and Dependency Management

  • Major Version Bump: The project version was upgraded from 15.x to 16.x to reflect major changes.
  • Spring Boot 4 Alignment: Updated parent POMs and library dependencies to align with Spring Boot 4 standards.
  • Dependency Cleanup:
    • Removed JUnit 4 dependencies in favor of JUnit 5.
    • Switched from spring-boot-starter-json to spring-boot-starter-jackson.
    • Completely excluded commons-logging to prevent it from “sneaking in” via other starters (e.g., spring-boot-test-starter).
  • Plugin Updates: Updated several Maven plugins, including versions-maven-plugin (to 2.20.1) and JaCoCo configurations to ensure compatibility with the new environment.

2. Jackson 3 Migration

  • Namespace Shift: Migrated from the traditional com.fasterxml.jackson namespace to the new tools.jackson namespace.
  • JsonLoader Refactoring: Updated JsonLoader to use JsonMapper instead of ObjectMapper.
  • New Configuration API:
    • Introduced JsonMapperPrototype to provide a standardized way to build “boot-like” mappers.
    • Added LimeJacksonJsonConfiguration which provides a @ConditionalOnMissingBean fallback for JsonMapper.
  • Behavioral Adjustments: Disabled FAIL_ON_NULL_FOR_PRIMITIVES by default to maintain “sane” behavior across the library’s typical use cases.

3. Testing and Validation Enhancements

  • New Validation Support: Introduced ValidationSupport and ValidationSupportConfiguration in test-utilities. This provides a fluent API (hardValidate) for asserting that DTOs and Beans satisfy Jakarta Bean Validation constraints (e.g., @NotNull) during tests.
  • Test Refactoring: Factorized validation logic and cleaned up various unit tests to match the new library versions.

4. Infrastructure and Docker

  • LocalStack Reliability: Updated docker-compose.yml across multiple modules to fix timing failures during CI/CD.
    • Increased start_period for health checks from 1s to 30s.
    • Enhanced health check commands to verify multiple services (sqss3sns) as they are started lazily in LocalStack.
    • Configured additional port ranges for LocalStack services.

5. External API Updates (GitHub)

  • GitHub API Migration: Updated the jcabi-github implementation in github-utilities.
    • Renamed usages of Github/RtGithub to GitHub/RtGitHub to match library changes.
    • Migrated from javax.json to jakarta.json namespaces for JSON processing within the GitHub integration.
  • Application Cleanup: Removed unused properties (like workflowRepositoryId) and added @SuppressWarnings for specific linting rules.

6. General Cleanup

  • Suppressed Javadoc missing warnings.
  • Removed unnecessary comments and usage of “ant export properties” in build configurations.
  • Corrected various Docker startup and timing issues that were causing flaky tests.

References

Efficient SQS Messaging in Spring Boot with SqsPump: A Complete Guide

The component provides a highly efficient way to send messages to AWS SQS queues in Spring Boot applications through intelligent batching and optimized throughput. This article explores how to integrate and leverage this powerful messaging utility in your Spring Boot applications. 

Our open source library, sqs-utilities, is available on GitHub here: sqs-utilities

Adding SQS Utilities to Your Maven Dependencies

To include the library in your Maven project, you need to add the appropriate dependency to your file. Based on the project structure, here’s how to configure it:

Maven Dependency Configuration

Add the following dependency to your file’s <dependencies> section: pom.xml

<dependencies>
<!-- SQS Utilities for AWS SQS messaging -->
<dependency>
<groupId>com.limemojito.oss.standards.aws</groupId>
<artifactId>sqs-utilities</artifactId>
<version>15.3.3</version>
</dependency>
</dependencies>
<dependencies>
    <!-- SQS Utilities for AWS SQS messaging -->
    <dependency>
        <groupId>com.limemojito.oss.standards.aws</groupId>
        <artifactId>sqs-utilities</artifactId>
        <version>15.3.3</version>
    </dependency>
</dependencies>

Getting Started with SqsPumpConfig

To enable SqsPump in your Spring Boot application, simply import the configuration class:

@SpringBootApplication
@Import(SqsPumpConfig.class)
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

The @Import(SqsPumpConfig.class) annotation automatically configures all necessary components:

  • The main batching component SqsPump
  • The underlying AWS SQS client wrapper SqsSender
  • JSON serialization with Spring Boot-like configuration ObjectMapper
  • AWS SDK v2 client (must be provided as a bean) SqsClient

Basic Usage in Your Components

Once configured, inject into any Spring component: SqsPump

@Service
public class OrderProcessingService {
    
    private final SqsPump sqsPump;
    private static final String ORDER_QUEUE_URL = "https://sqs.region.amazonaws.com/account/order-queue";
    
    public OrderProcessingService(SqsPump sqsPump) {
        this.sqsPump = sqsPump;
    }
    
    public void processOrder(Order order) {
        // Add order to batch - efficient, non-blocking
        sqsPump.send(ORDER_QUEUE_URL, order);
        
        // Batch will be sent automatically when full or via explicit flush
    }
    
    public void forceFlushPendingOrders() {
        sqsPump.flush(ORDER_QUEUE_URL);
    }
}

Understanding Batch Efficiency

The core efficiency of comes from its intelligent batching mechanism powered by the SqsSender.sendBatch() method. Here’s how it optimizes throughput: SqsPump

Note that you should flush(destination) on SqsPump at the end of your batch processing to clear the batch queue.

Automatic Batching Strategy

// Configuration
com.limemojito.sqs.batchSize=10 # Default batch size

The pump accumulates messages until:

  1. Batch size reached: When 10 messages (default) are queued
  2. Explicit flush: When you call flush(destination)
  3. Application shutdown: Via lifecycle hook @PreDestroy

Efficiency Calculations

For high-volume scenarios, the efficiency gains are significant:

Without Batching (Individual sends):

  • 1000 messages = 1000 API calls
  • Each call: ~50-100ms latency
  • Total time: 50-100 seconds

With SqsPump Batching:

  • 1000 messages = 100 batch calls (10 messages each)
  • Each batch call: ~50-100ms latency
  • Total time: 5-10 seconds
  • 90% reduction in execution time

Cost Optimization

AWS SQS pricing is per request, making batching extremely cost-effective:

  • Individual sends: 1000 requests × 0.0000004 =0.0004
  • Batch sends: 100 requests × 0.0000004 =0.00004
  • 90% cost reduction

Advanced Usage Patterns

Multi-Queue Processing

@Service
public class EventPublisher {
    
    private final SqsPump sqsPump;
    
    public void publishEvents(List<Event> events) {
        for (Event event : events) {
            String queueUrl = determineQueueForEvent(event);
            sqsPump.send(queueUrl, event);
        }
        
        // Flush all queues at once for maximum efficiency when using multiple destinations. Use flush(queueUrl) for single destination.
        sqsPump.flushAll();
    }
}

FIFO Queue Support

For ordered message processing:

@Service
public class SequentialProcessor {
    
    private final SqsPump sqsPump;
    private static final String FIFO_QUEUE = "https://sqs.region.amazonaws.com/account/ordered-queue.fifo";
    
    public void sendOrderedMessage(String groupId, List<Object> messages, String deduplicationId) {
        Map<String, Object> fifoHeaders = Map.of(
            "message-group-id", groupId,
            "message-deduplication-id", deduplicationId
        );
        messages.forEach(message -> sqsPump.send(FIFO_QUEUE, message, fifoHeaders));
        // send any remaining messages
        sqsPump.flush(FIFO_QUEUE);
    }
}

Thread Safety and Concurrency

is designed for high-concurrency environments:

Key Thread Safety Features:

  • SqsPump: Thread-safe message storage per destination ConcurrentHashMap
  • :SqsPump: Lock-free message queuing ConcurrentLinkedDeque
  • Synchronized flush: Only one thread flushes per destination at a time, no explicit coding necessary.
  • Atomic batch operations: Complete batches or failure with rollback

Configuration Options

Application Properties

# application.yml
com:
limemojito:
sqs:
batchSize: 10 # Max messages per batch (default: 10, max: 10 per AWS limits)

# AWS SQS Client configuration
aws:
region: us-west-2
credentials:
accessKey: ${AWS_ACCESS_KEY}
secretKey: ${AWS_SECRET_KEY}

Custom SQS Client Configuration

@Configuration
public class AwsConfig {

@Bean
public SqsClient sqsClient() {
return SqsClient.builder()
.region(Region.US_WEST_2)
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
}
}

Message Attributes and Spring Compatibility

SqsPump automatically adds Spring Messaging-compatible attributes: 

{
"MessageAttributes": {
"id": "uuid-generated",
"timestamp": "1640995200000",
"contentType": "application/json",
"Content-Type": "application/json",
"Content-Length": "156"
}
}

These attributes ensure seamless integration with:

  • Spring Cloud Stream
  • Spring Integration
  • Spring Boot messaging auto-configuration

Best Practices

1. Flush when needed

After looping, always flush. Messages may be in memory before delivery.

messages.forEach(message -> sqsPump.send(FIFO_QUEUE, message, fifoHeaders));
// send any remaining messages
sqsPump.flush(FIFO_QUEUE);

2. Use Explicit Flushing for Critical Messages

// For time-sensitive messages
sqsPump.send(queueUrl, criticalMessage);
sqsPump.flush(queueUrl); // Immediate send

3. Batch Size Optimization

// For high-throughput: use max batch size
com.limemojito.sqs.batchSize=10

// For low-latency: use smaller batches
com.limemojito.sqs.batchSize=3

4. Lifecycle Management

SqsPump automatically flushes pending messages on container shutdown in @PreDestroy.

Conclusion

transforms SQS messaging in Spring Boot applications by providing:

  • 90% reduction in API calls through intelligent batching
  • Significant cost savings via reduced request counts
  • Thread-safe concurrent message handling
  • Spring Boot integration with zero configuration overhead
  • Automatic lifecycle management preventing message loss

By leveraging SqsSender.sendBatch() under the hood, delivers enterprise-grade performance while maintaining the simplicity that Spring Boot developers expect. Whether you’re processing thousands of messages per second or need reliable ordered delivery via FIFO queues, provides the foundation for scalable, efficient AWS SQS integration. SqsPumpSqsPump

The combination of automatic batching, thread safety, and Spring Boot’s dependency injection makes an ideal choice for modern cloud-native applications requiring high-performance message queuing.

For information on the testing of SQSPump, see our article here.

Testing High Performance AWS SQS batch sending

The class, from our OSS Maven standards project, is a well-structured unit test suite that demonstrates testing patterns for AWS SQS batch messaging functionality. As a senior Java developer, you’ll appreciate the sophisticated test design patterns and AWS integration testing techniques employed in this test class.

See the test code here on GitHub: SqsPumpTest.java

For more information on our open source SQSPump, see our article here.

Test Architecture Overview

The test class follows modern Java testing best practices using JUnit 5, Mockito, and AssertJ. It’s designed to test the component, which is a utility for efficiently sending multiple messages to AWS SQS queues in batches using SqsPump. The code for SqsPump is available GitHub: SqsPump.java

Key Testing Components

Test Setup:

  • Uses for clean dependency injection @ExtendWith(MockitoExtension.class)
  • Mocks the to isolate the unit under test SqsClient
  • Mockito verify the exact parameters passed to AWS SDK calls ArgumentCaptor
  • Configures a controlled batch size of 10 messages for predictable testing

Core Dependencies:

  • The main class being tested SqsPump
  • A wrapped AWS SQS client with JSON serialization capabilities SqsSender
  • Jackson : Configured with boot-like settings for JSON processing ObjectMapper

Test Scenarios Explained

1. Batch Size Boundary Testing

The test suite includes three critical batch tests that demonstrate the pump’s ability to handle different message volumes:

  • Exact batch size (10 messages): Verifies optimal batching behavior
  • Large volume (100 messages): Tests multiple batch processing
  • Irregular size (33 messages): Ensures proper handling of partial final batches

These tests use the performBatchTest() method, which:

  1. Sends N objects to the pump TestMessage
  2. Triggers a flush operation
  3. Verifies the exact number of batch calls to SQS
  4. Validates that messages are properly batched and serialized

2. FIFO Queue Support

The test demonstrates support for FIFO (First-In-First-Out) queues, which require: shouldSendAFifoMessage()

  • Message Deduplication ID: Prevents duplicate processing
  • Message Group ID: Ensures ordering within message groups

This test verifies that FIFO-specific headers are correctly mapped to AWS SQS batch request parameters.

3. Edge Case Handling

The test ensures the pump optimizes for empty batches by not making unnecessary AWS API calls – a crucial performance consideration. shouldNotPumpZeroMessages()

Testing Patterns Worth Noting

1. Argument Capturing Pattern

@Captor
private ArgumentCaptor<SendMessageBatchRequest> requestCaptor;

This pattern allows precise verification of complex objects passed to mocked dependencies without exposing internal implementation details.

2. Batch Calculation Logic

The expectedBatchSends() method demonstrates a clean mathematical approach to calculating expected batch counts:

private int expectedBatchSends(int sendSize) {
return sendSize / pumpMaxBatchSize + (sendSize % pumpMaxBatchSize > 0 ? 1 : 0);
}

3. Message Attribute Validation

The test includes comprehensive validation of SQS message attributes, ensuring proper content-type headers, timestamps, and content length metadata are set correctly.

AWS SDK Integration Insights

The test reveals several important aspects of AWS SQS batch processing:

  • Batch Request Structure: Each batch contains multiple objects SendMessageBatchRequestEntry
  • Message Serialization: JSON serialization with proper content-type headers
  • Attribute Metadata: Automatic inclusion of timestamps and content length
  • ID Management: Sequential ID assignment for batch entries

Key Testing Techniques for Senior Developers

  1. Mock Strategy: Only the AWS client is mocked, allowing real business logic to execute
  2. Data-Driven Testing: Multiple test cases with different batch sizes validate edge cases
  3. Comprehensive Assertions: Both structural (batch count) and content (message bodies) validation
  4. Performance Considerations: Zero-message optimization testing prevents unnecessary API calls

Conclusion

This test suite exemplifies professional-grade testing for cloud-native Java applications. It demonstrates how to effectively test complex batch processing logic while maintaining clear test isolation and comprehensive coverage. The patterns used here are directly applicable to testing other AWS service integrations and batch processing scenarios in enterprise Java applications.

The test validates FIFO queue support, message attributes, and batch boundary conditions showing the maturity expected in production AWS integrations, making this an excellent reference for similar testing challenges.

Building Cluster-Safe Once-Only Methods with Locks, Java and Postgres or DynamoDB

In distributed systems, ensuring that certain operations execute only once across multiple instances is a critical requirement. Whether you’re processing payments, sending notifications, or performing data migrations, you need guarantees that these operations don’t accidentally run multiple times. This article explores how to use PostgreSQL advisory locks through the lock-postgres utility to create cluster-safe once-only methods in Java.

See here for our OSS implementation of cluster locks using postgresql. We also have an implementation using DynamoDB here (same API).

The Challenge of Distributed Execution

Consider a common scenario: you have multiple instances of your application running in a cluster, and each instance processes scheduled tasks. Without proper coordination, you might end up with:

  • Duplicate payment processing
  • Multiple notification emails sent
  • Race conditions in data migrations
  • Resource contention issues

Traditional Java synchronization mechanisms like synchronized blocks or ReentrantLock only work within a single JVM. For cluster-wide coordination, you need a distributed locking mechanism.

Solution 1: Using PostgreSQL Advisory Locks

PostgreSQL provides advisory locks – lightweight, application-level locks that don’t interfere with table-level locks. These locks are perfect for coordinating application logic across multiple instances.

The lock-postgres utility leverages PostgreSQL’s advisory lock functions:

  • pg_try_advisory_xact_lock(key) – Non-blocking lock attempt
  • pg_advisory_xact_lock(key) – Blocking lock acquisition

These locks are automatically released when the database transaction commits or rolls back, making them ideal for transactional operations.

Solution 2: Using DynamoDB and the AWS AmazonDynamoDBLockClient

We also have a dynamo DB solution using the AWS AmazonDynamoDBLockClient as the implementation of the Lock API. This is an implementation of the same lock API in the examples in this article.

Setting Up the Dependencies

First, add the required dependencies to your project:

PostgreSQL implementation:

<dependency>   
  <groupId>com.limemojito.oss.standards.lock</groupId>  
  <artifactId>lock-postgres</artifactId>
  <version>15.3.2</version>
</dependency>

DynamoDB Implementation

<dependency>   
  <groupId>com.limemojito.oss.standards.lock</groupId>  
  <artifactId>lock-dynamodb</artifactId>
  <version>15.3.2</version>
</dependency>

Basic Usage Pattern

The PostgresLockService implements the LockService interface and provides two primary methods for lock acquisition:

@Service
@RequiredArgsConstructor
public class OnceOnlyService {   
     private final LockService lockService;   
     private final PaymentProcessor paymentProcessor;        
     @Transactional    public void processPaymentOnceOnly(String paymentId) {
        String lockName = "payment-processing-" + paymentId;
        // Try to acquire the lock - non-blocking
        Optional<DistributedLock> lock = lockService.tryAcquire(lockName);
        if (lock.isPresent()) {           
          try (DistributedLock distributedLock = lock.get()) {                
                // Only one instance will execute this block
                paymentProcessor.process(paymentId);
                log.info("Payment {} processed successfully", paymentId);
           }
        } else {
            log.info("Payment {} is already being processed by another instance", paymentId);
        }
    }
}

Blocking vs Non-Blocking Lock Acquisition

The lock service provides two approaches:

1. Non-Blocking (tryAcquire)

@Transactional
public void tryProcessOnceOnly(String taskId) {
    Optional<DistributedLock> lock = lockService.tryAcquire("task-" + taskId);
        if (lock.isPresent()) {
        try (DistributedLock distributedLock = lock.get()) {
            // Process the task
            performCriticalOperation(taskId);
        }
    } else {
        // Task is being processed elsewhere, skip or handle accordingly
        log.info("Task {} is already being processed", taskId);
    }
}

2. Blocking (acquire)

@Transactional
public void waitAndProcessOnceOnly(String taskId) {
    // This will wait until the lock becomes available
    try (DistributedLock lock = lockService.acquire("task-" + taskId)) {
        // Guaranteed to execute once the lock is acquired
        performCriticalOperation(taskId);
    }
    // Lock is automatically released when the transaction commits
}

Real-World Example: Daily Report Generation

Let’s implement a practical example where multiple application instances need to coordinate daily report generation:

@Component
@RequiredArgsConstructor
@Slf4j
public class DailyReportService {
    private final LockService lockService;
    private final ReportRepository reportRepository;
    private final NotificationService notificationService;

    @Scheduled(cron = "0 0 2 * * *") // Run at 2 AM daily
    @Transactional
    public void generateDailyReport() {
        String today = LocalDate.now().toString();
        String lockName = "daily-report-" + today;
        Optional<DistributedLock> lock = lockService.tryAcquire(lockName);
        if (lock.isPresent()) {
            try (DistributedLock distributedLock = lock.get()) {
                log.info("Starting daily report generation for {}", today);

                // Check if report already exists (additional safety)
                if (reportRepository.existsByDate(today)) {
                    log.info("Report for {} already exists, skipping", today);
                    return;
                }
 
                // Generate the report
                Report report = generateReport(today);
                reportRepository.save(report);
 
                // Send notifications
                notificationService.sendReportGeneratedNotification(report);
                log.info("Daily report for {} generated successfully", today);
            }
        } else {
            log.info("Daily report for {} is being generated by another instance", today);
        }
    }

    private Report generateReport(String date) {
        // Implementation of report generation logic
        return new Report(date, collectDailyMetrics());
    }
}

Advanced Patterns

1. Lock with Timeout Handling

For blocking locks, you can implement timeout handling using Spring’s @Transactional timeout:

@Transactional(timeout = 30) // 30-second timeout
public void processWithTimeout(String taskId) {
    try (DistributedLock lock = lockService.acquire("timeout-task-" + taskId)) {
        performLongRunningOperation(taskId);
    } catch (DataAccessException e) {
        log.error("Failed to acquire lock within timeout period", e);
        throw new LockTimeoutException("Could not acquire lock for task: " + taskId);
    }
}

2. Hierarchical Locking

Create hierarchical locks for complex operations:

@Transactional
public void processOrderWithHierarchy(String customerId, String orderId) {
    // First acquire customer-level lock
    try (DistributedLock customerLock = lockService.acquire("customer-" + customerId)) {
        // Then acquire order-level lock
        try (DistributedLock orderLock = lockService.acquire("order-" + orderId)) {
            processOrderSafely(customerId, orderId);
        }
    }
}

3. Conditional Processing with Fallback

@Transactional
public ProcessingResult processWithFallback(String taskId) {
    Optional<DistributedLock> lock = lockService.tryAcquire("primary-task-" + taskId);
    if (lock.isPresent()) {
        try (DistributedLock distributedLock = lock.get()) {
            return performPrimaryProcessing(taskId);
        }
    } else {
        // Primary processing is happening elsewhere, perform alternative action
        return performAlternativeProcessing(taskId);
    }
}

Configuration and Best Practices

1. Database Configuration

Ensure your PostgreSQL database is properly configured for advisory locks:

-- Check current lock status
SELECT * FROM pg_locks WHERE locktype = 'advisory';
-- Set appropriate connection and statement timeouts
SET statement_timeout = '30s';
SET lock_timeout = '10s';

2. Spring Configuration

Configure your PostgresLockService bean:

@Configuration
public class LockConfiguration {
    @Bean
    public LockService lockService(JdbcTemplate jdbcTemplate) {
        return new PostgresLockService(jdbcTemplate);
    }
}

Key Benefits and Considerations

Benefits:

  • Cluster-safe: Works across multiple JVM instances
  • Transactional: Automatically releases locks on transaction completion
  • Lightweight: No additional infrastructure required
  • Reliable: Leverages PostgreSQL’s proven lock mechanisms
  • Flexible: Supports both blocking and non-blocking approaches

Considerations:

  • Database dependency: Requires PostgreSQL database connection
  • Transaction requirement: Locks must be used within database transactions
  • Lock key collision: Different lock names with same hash could collide
  • Connection pooling: Consider impact on database connection pools

Conclusion

PostgreSQL advisory locks provide a robust foundation for implementing cluster-safe once-only methods in Java applications. The lock-postgres utility simplifies this implementation by providing a clean API that integrates seamlessly with Spring’s transaction management.

By using these distributed locks, you can ensure that critical operations execute exactly once across your entire cluster, preventing data inconsistencies and duplicate processing. The transaction-based approach ensures that locks are automatically cleaned up, even in failure scenarios, making your distributed system more reliable and maintainable.

Whether you’re processing financial transactions, generating reports, or coordinating data migrations, PostgreSQL advisory locks offer a battle-tested solution for distributed coordination without the complexity of additional infrastructure components.

Using AWS Cognito, API Gateway and Spring Cloud Function Lambda for security authorisation.

This article explains using our OSS lambda-utilities to configure a spring cloud function java lambda to allow method level authorisation using API Gateway and Cognito.

See our OSS repository here.

Architecture Overview

The security setup integrates three key AWS services:

  1. AWS Cognito – Identity provider and JWT issuer
  2. AWS API Gateway – HTTP API with JWT authorizer
  3. AWS Lambda – Function execution environment

Key Components

1. ApiGatewayResponseDecoratorFactory

This is the central factory that creates decorated Spring Cloud Functions with security and error handling:

@Service
public class ApiGatewayResponseDecoratorFactory {
// Creates decorated functions that handle security, errors, and responses
public <Input, Output> Function<Input, APIGatewayV2HTTPResponse> create(Function<Input, Output> function)
}

Purpose:

  • Wraps your business logic functions
  • Automatically handles authentication extraction from API Gateway events
  • Converts exceptions to proper HTTP responses
  • Manages Spring Security context

2. Security Configuration Setup

The security is configured through : AwsCloudFunctionSpringSecurityConfiguration

@EnableMethodSecurity
@Configuration
@Import({LimeJacksonJsonConfiguration.class, ApiGatewayResponseDecoratorFactory.class})
@ComponentScan(basePackageClasses = ApiGatewayAuthenticationMapper.class)
public class AwsCloudFunctionSpringSecurityConfiguration

This enables:

  • Method-level security (@PreAuthorize@Secured, etc.)
  • Automatic authentication mapping
  • Exception handling for security violations

3. Authentication Flow

The authentication process works as follows:

  1. API Gateway receives request with JWT token in Authorization header
  2. JWT Authorizer validates the token against Cognito
  3. API Gateway forwards the validated JWT claims in the request context
  4. extracts authentication from the event: 
    • Reads JWT claims from request context
    • Creates object ApiGatewayAuthentication
    • Maps Cognito groups to Spring Security authorities
    ApiGatewayAuthenticationMapper
  5. Spring Security context is populated for method-level security

AWS Infrastructure Setup

API Gateway Configuration

# Example CDK/CloudFormation for HTTP API with JWT Authorizer
HttpApi:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: MySecureApi
ProtocolType: HTTP

JwtAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
ApiId: !Ref HttpApi
AuthorizerType: JWT
IdentitySource:
- $request.header.Authorization
JwtConfiguration:
Audience:
- your-cognito-client-id
Issuer: https://cognito-idp.{region}.amazonaws.com/{user-pool-id}

Route:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref HttpApi
RouteKey: POST /secure-endpoint
Target: !Sub integrations/${LambdaIntegration}
AuthorizerId: !Ref JwtAuthorizer
AuthorizationType: JWT

Cognito Configuration

UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: MyAppUsers
Schema:
- Name: email
AttributeDataType: String
Required: true
Policies:
PasswordPolicy:
MinimumLength: 8

UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
ClientName: MyAppClient
GenerateSecret: false
ExplicitAuthFlows:
- ADMIN_NO_SRP_AUTH
- USER_PASSWORD_AUTH

Implementation Example

1. Create Your Business Function

@Component
public class SecureBusinessLogic {

public String processSecureData(MyRequest request) {
// Your business logic here
return "Processed: " + request.getData();
}
}

2. Create the Lambda Handler

@Configuration
@Import(LimeAwsLambdaConfiguration.class)
public class LambdaConfiguration {

@Autowired
private ApiGatewayResponseDecoratorFactory decoratorFactory;

@Autowired
private SecureBusinessLogic businessLogic;

@Bean
public Function<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> secureFunction() {
return decoratorFactory.create(event -> {
// Extract request body
MyRequest request = parseRequest(event.getBody());

// Business logic with automatic security context
return businessLogic.processSecureData(request);
});
}
}

3. Add Method-Level Security

@Component
public class SecureBusinessLogic {

@PreAuthorize("hasAuthority('ADMIN')")
public String processAdminData(MyRequest request) {
return "Admin processed: " + request.getData();
}

@PreAuthorize("hasAuthority('USER') or hasAuthority('ADMIN')")
public String processUserData(MyRequest request) {
return "User processed: " + request.getData();
}
}

4. Access Current User Context

@Bean
public Function<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> contextAwareFunction() {
return decoratorFactory.create(event -> {
// Access current authentication
ApiGatewayContext context = decoratorFactory.getCurrentApiGatewayContext();
ApiGatewayAuthentication auth = context.getAuthentication();

if (auth.isAuthenticated()) {
String username = auth.getPrincipal().getName();
Set<String> groups = auth.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());

return new UserResponse(username, groups, "Success");
} else {
return new UserResponse("anonymous", Set.of("ANONYMOUS"), "Limited access");
}
});
}

Configuration Properties

The authentication mapper supports several configuration properties:

com:
limemojito:
aws:
lambda:
security:
claimsKey: "cognito:groups" # Cognito groups claim
anonymous:
sub: "ANONYMOUS"
userName: "anonymous"
authority: "ANONYMOUS"

Security Benefits

  1. Automatic JWT Validation: API Gateway validates tokens before reaching Lambda
  2. Claims Extraction: Automatic mapping of Cognito user groups to Spring authorities
  3. Method Security: Use standard Spring Security annotations
  4. Exception Handling: Automatic conversion of security exceptions to HTTP responses
  5. Context Access: Easy access to user information and claims
  6. Anonymous Support: Graceful handling of unauthenticated requests

Error Handling

The decorator automatically handles:

  • Authentication failures → 401 Unauthorized
  • Authorization failures → 403 Forbidden
  • Validation errors → 400 Bad Request
  • General exceptions → 500 Internal Server Error

This architecture provides a robust, scalable security solution that leverages AWS managed services while maintaining clean separation of concerns in your Spring Cloud Function implementation.

Debugging Maven Projects with Conflicting JAR Versions

Maven dependency conflicts are one of the most frustrating issues developers encounter when building Java applications. When multiple versions of the same library exist in your classpath, it can lead to runtime errors, unexpected behavior, and difficult-to-debug issues. This article provides a comprehensive guide to identifying, understanding, and resolving JAR version conflicts in Maven projects.

Understanding Dependency Conflicts

What Are Dependency Conflicts?

Dependency conflicts occur when your project’s dependency tree contains multiple versions of the same artifact (same groupId and artifactId but different versions). Maven’s dependency resolution mechanism will choose one version based on its rules, but this choice might not be compatible with all parts of your application.

Common Symptoms

  • ClassNotFoundException or NoClassDefFoundError at runtime
  • NoSuchMethodError or AbstractMethodError
  • IncompatibleClassChangeError
  • Unexpected behavior in libraries that worked in isolation
  • Different behavior between development and production environments

Identifying Conflicts

1. Using Maven Dependency Plugin

The most effective way to identify conflicts is using Maven’s built-in dependency plugin:

mvn dependency:tree

This command shows your complete dependency tree. Look for multiple versions of the same artifact:

[INFO] +- com.fasterxml.jackson.core:jackson-core:jar:2.13.0:compile
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.0:compile
[INFO] |  \- com.fasterxml.jackson.core:jackson-core:jar:2.12.0:compile (omitted for conflict with 2.13.0)

2. Analyzing Conflicts with Verbose Output

For more detailed conflict analysis:

mvn dependency:tree -Dverbose

This shows which dependencies are omitted due to conflicts and why Maven chose specific versions.

3. Using the Dependency Analyze Goal

mvn dependency:analyze

This command identifies:

  • Used undeclared dependencies
  • Unused declared dependencies
  • Potential conflicts

4. IDE-Based Analysis

Most modern IDEs provide visual dependency analysis:

  • IntelliJ IDEA: Right-click on pom.xml → Analyze Dependencies
  • Eclipse: Project Properties → Java Build Path → Libraries → Maven Dependencies

Understanding Maven’s Resolution Strategy

Maven uses these rules to resolve conflicts:

  1. Nearest Definition: Dependencies closer to the root in the dependency tree win
  2. First Declaration: If dependencies are at the same depth, the first one declared wins
  3. Version Range: Explicit version ranges override transitive dependencies

Resolution Strategies

1. Explicit Dependency Declaration

The most straightforward approach is to explicitly declare the version you want:

<dependencies>    
  <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.13.0</version>
  </dependency>
</dependencies>

2. Dependency Management Section

Use the <dependencyManagement> section to centrally manage versions:

<dependencyManagement>    
  <dependencies>        
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId> 
      <version>2.13.0</version>   
    </dependency>
  </dependencies>
</dependencyManagement>

3. Excluding Transitive Dependencies

Exclude problematic transitive dependencies:

<dependency>    
  <groupId>org.springframework</groupId>  
  <artifactId>spring-web</artifactId>   
  <version>5.3.0</version>  
  <exclusions>
     <exclusion>     
        <groupId>com.fasterxml.jackson.core</groupId>       
        <artifactId>jackson-core</artifactId>  
        </exclusion>  
     </exclusions>
</dependency>

4. Using Maven Enforcer Plugin

Prevent conflicts by failing the build when they occur:

<plugin>    
  <groupId>org.apache.maven.plugins</groupId> 
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>3.0.0</version> 
  <executions>       
    <execution>
       <id>enforce-no-duplicate-dependencies</id>        
       <goals>            
         <goal>enforce</goal>       
       </goals>  
       <configuration>       
          <rules>                
            <dependencyConvergence/>        
            <requireNoRepositories/>           
          </rules>  
       </configuration>   
     </execution>
   </executions>
</plugin>

Advanced Debugging Techniques

1. Creating a Dependency Report

Generate detailed dependency reports:

mvn project-info-reports:dependencies

This creates an HTML report showing all dependencies and their relationships.

2. Using Maven’s Debug Output

Run Maven with debug output to see detailed resolution information:

mvn -X dependency:tree

3. Checking Effective POM

View the effective POM to see resolved dependencies:

mvn help:effective-pom

Best Practices

1. Use Bill of Materials (BOM)

Import BOMs for consistent dependency versions:

<dependencyManagement>  
  <dependencies>       
    <dependency>       
      <groupId>org.springframework.boot</groupId>    
      <artifactId>spring-boot-dependencies</artifactId>      
      <version>2.7.0</version>     
      <type>pom</type>      
      <scope>import</scope>      
    </dependency>    
  </dependencies>
</dependencyManagement>

2. Regular Dependency Updates

Keep dependencies up to date and use tools like:

  • mvn versions:display-dependency-updates
  • mvn versions:use-latest-releases

3. Minimize Direct Dependencies

Reduce the number of direct dependencies to minimize conflict opportunities.

4. Use Dependency Scopes Appropriately

  • compile: Default scope
  • provided: Available at compile time but not packaged
  • runtime: Not needed for compilation but required at runtime
  • test: Only available during testing

Preventing Future Conflicts

1. Establish Dependency Governance

  • Create a team-wide dependency management strategy
  • Use parent POMs for version consistency
  • Regular dependency audits

2. Automated Conflict Detection

Integrate conflict detection into your CI/CD pipeline:

<plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-dependency-plugin</artifactId>    <executions>        <execution>            <goals>                <goal>analyze-only</goal>            </goals>            <configuration>                <failOnWarning>true</failOnWarning>            </configuration>        </execution>    </executions></plugin>

3. Version Range Strategy

Be cautious with version ranges. Prefer specific versions for stability:

<!-- Avoid --><version>[1.0,2.0)</version>
<!-- Prefer --><version>1.5.2</version>

Common Conflict Scenarios

Spring Framework Conflicts

Spring projects often have complex dependency trees. Use Spring Boot’s dependency management or Spring Framework BOM.

Logging Framework Conflicts

Multiple logging frameworks (Log4j, Logback, Commons Logging) often conflict. Use SLF4J as a facade and bridge other frameworks.

Jackson Library Conflicts

Jackson modules must use compatible versions. Manage them centrally in dependencyManagement.

Conclusion

Debugging Maven dependency conflicts requires a systematic approach:

  1. Identify conflicts using Maven tools
  2. Understand Maven’s resolution strategy
  3. Apply appropriate resolution techniques
  4. Prevent future conflicts with good practices

The key is to be proactive rather than reactive. Establish good dependency management practices early in your project lifecycle, and use automated tools to catch conflicts before they reach production.

Remember that dependency conflicts are often symptoms of deeper architectural issues. Sometimes the best solution is to refactor your application to reduce complex dependency chains rather than working around conflicts with exclusions and forced versions.

By following these practices and using the tools outlined in this article, you’ll be well-equipped to handle even the most complex dependency conflict scenarios in your Maven projects.