Introduction
Karate is a powerful open-source tool that combines API test automation, performance testing, and mocks into a single framework. Unlike traditional testing frameworks, Karate uses a behavior-driven development (BDD) syntax that makes tests readable and maintainable.
Setup and Dependencies
1. Maven Configuration
<properties> <karate.version>1.4.1</karate.version> <maven.surefire.version>3.0.0-M9</maven.surefire.version> </properties> <dependencies> <!-- Karate Core --> <dependency> <groupId>com.intuit.karate</groupId> <artifactId>karate-core</artifactId> <version>${karate.version}</version> <scope>test</scope> </dependency> <!-- Karate JUnit 5 --> <dependency> <groupId>com.intuit.karate</groupId> <artifactId>karate-junit5</artifactId> <version>${karate.version}</version> <scope>test</scope> </dependency> <!-- Karate Apache - for HTTP client --> <dependency> <groupId>com.intuit.karate</groupId> <artifactId>karate-apache</artifactId> <version>${karate.version}</version> <scope>test</scope> </dependency> <!-- Test Containers for integration testing --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.19.3</version> <scope>test</scope> </dependency> <!-- Rest Assured for comparison (optional) --> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <version>5.3.2</version> <scope>test</scope> </dependency> <!-- Jackson for JSON parsing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> </dependencies> <build> <testResources> <testResource> <directory>src/test/java</directory> <excludes> <exclude>**/*.java</exclude> </excludes> </testResource> <testResource> <directory>src/test/resources</directory> </testResource> </testResources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>${maven.surefire.version}</version> <configuration> <includes> <include>**/*Test.java</include> <include>**/*TestRunner.java</include> </includes> </configuration> </plugin> </plugins> </build> 2. Gradle Configuration
dependencies { testImplementation("com.intuit.karate:karate-core:1.4.1") testImplementation("com.intuit.karate:karate-junit5:1.4.1") testImplementation("com.intuit.karate:karate-apache:1.4.1") testImplementation("org.testcontainers:testcontainers:1.19.3") testImplementation("io.rest-assured:rest-assured:5.3.2") } sourceSets { test { resources { srcDir file('src/test/java') exclude '**/*.java' } } } test { useJUnitPlatform() systemProperty "karate.options", System.getProperty("karate.options") systemProperty "karate.env", System.getProperty("karate.env", "dev") outputs.upToDateWhen { false } } Project Structure
src/test/java/ ├── com/example/tests/ │ ├── runners/ │ │ ├── TestRunner.java │ │ ├── UserTestRunner.java │ │ └── OrderTestRunner.java │ ├── helpers/ │ │ ├── JavaHelpers.java │ │ └── DatabaseUtils.java │ └── utils/ │ ├── TestDataGenerator.java │ └── EmailValidator.java └── com/example/features/ ├── common/ │ ├── common.feature │ ├── headers.feature │ └── auth.feature ├── users/ │ ├── users.feature │ ├── user-create.feature │ └── user-get.feature ├── orders/ │ ├── orders.feature │ ├── order-create.feature │ └── order-update.feature ├── payments/ │ └── payments.feature └── performance/ └── load-test.feature
Core Karate Features
1. Basic API Test Structure
# users/user-create.feature Feature: User Management API - Create User Background: * url baseUrl * configure headers = { 'Content-Type': 'application/json', 'X-Request-ID': '#(requestId)' } * def requestId = function() { return java.util.UUID.randomUUID() + '' } Scenario: Create a new user with valid data Given path 'users' And request """ { "firstName": "John", "lastName": "Doe", "email": "[email protected]", "phone": "+1234567890", "dateOfBirth": "1990-01-01" } """ When method post Then status 201 And match response == """ { "id": "#number", "firstName": "John", "lastName": "Doe", "email": "[email protected]", "phone": "+1234567890", "dateOfBirth": "1990-01-01", "createdAt": "#notnull", "updatedAt": "#notnull" } """ And def userId = response.id Scenario: Create user with duplicate email should fail Given path 'users' And request """ { "firstName": "Jane", "lastName": "Smith", "email": "[email protected]", "phone": "+1234567891" } """ When method post Then status 409 And match response == """ { "errorCode": "USER_ALREADY_EXISTS", "message": "#string", "timestamp": "#notnull" } """ Scenario: Create user with invalid email format Given path 'users' And request """ { "firstName": "Invalid", "lastName": "Email", "email": "invalid-email", "phone": "+1234567892" } """ When method post Then status 400 And match response.errorCode == 'VALIDATION_ERROR' And match response.errors contains 'email' 2. Data-Driven Testing
# users/user-create-datadriven.feature Feature: User Creation - Data Driven Tests Background: * url baseUrl * configure headers = { 'Content-Type': 'application/json' } Scenario Outline: Create user with various data combinations Given path 'users' And request """ { "firstName": "<firstName>", "lastName": "<lastName>", "email": "<email>", "phone": "<phone>" } """ When method post Then status <expectedStatus> And match response <expectedResponse> Examples: | firstName | lastName | email | phone | expectedStatus | expectedResponse | | John | Doe | [email protected] | +1234567890 | 201 | { id: '#number', email: '[email protected]' } | | Jane | Smith | invalid-email | +1234567891 | 400 | { errorCode: 'VALIDATION_ERROR' } | | | Johnson | [email protected] | +1234567892 | 400 | { errorCode: 'VALIDATION_ERROR' } | | Alice | | [email protected] | +1234567893 | 400 | { errorCode: 'VALIDATION_ERROR' } | | Bob | Brown | [email protected] | +1234567894 | 409 | { errorCode: 'USER_ALREADY_EXISTS' } | Scenario: Create multiple users from JSON array * def users = read('classpath:com/example/features/users/test-data/users.json') * def results = [] * eval for(var i = 0; i < users.length; i++) """ var user = users[i]; karate.call('user-create-single.feature', { userData: user }); results.push(response); """ * match each results contains { id: '#number', status: 'ACTIVE' } 3. Complex Test Scenarios
# orders/order-workflow.feature Feature: Complete Order Workflow Background: * url baseUrl * configure headers = { 'Content-Type': 'application/json' } * def createUser = read('classpath:com/example/features/common/create-user.feature') * def createProduct = read('classpath:com/example/features/common/create-product.feature') Scenario: Complete order placement and payment workflow # Step 1: Create a user Given call createUser { firstName: 'Order', lastName: 'User', email: '[email protected]' } And def userId = response.id # Step 2: Create products Given call createProduct { name: 'Test Product 1', price: 29.99, stock: 10 } And def productId1 = response.id Given call createProduct { name: 'Test Product 2', price: 49.99, stock: 5 } And def productId2 = response.id # Step 3: Create order Given path 'orders' And request """ { "userId": "#(userId)", "items": [ { "productId": "#(productId1)", "quantity": 2 }, { "productId": "#(productId2)", "quantity": 1 } ], "shippingAddress": { "street": "123 Main St", "city": "New York", "state": "NY", "zipCode": "10001", "country": "USA" } } """ When method post Then status 201 And def orderId = response.id And match response.status == 'PENDING' And match response.totalAmount == 109.97 # Step 4: Process payment Given path 'payments' And request """ { "orderId": "#(orderId)", "paymentMethod": "CREDIT_CARD", "amount": 109.97, "cardDetails": { "number": "4111111111111111", "expiryMonth": "12", "expiryYear": "2025", "cvv": "123" } } """ When method post Then status 201 And def paymentId = response.id And match response.status == 'COMPLETED' # Step 5: Verify order status updated Given path 'orders', orderId When method get Then status 200 And match response.status == 'CONFIRMED' # Step 6: Verify inventory updated Given path 'products', productId1 When method get Then status 200 And match response.stock == 8 Given path 'products', productId2 When method get Then status 200 And match response.stock == 4 Java Integration and Helpers
1. Test Runner Classes
package com.example.tests.runners; import com.intuit.karate.junit5.Karate; class TestRunner { @Karate.Test Karate testAll() { return Karate.run().relativeTo(getClass()); } @Karate.Test Karate testUsers() { return Karate.run("classpath:com/example/features/users") .tags("@users") .outputCucumberJson(true); } @Karate.Test Karate testOrders() { return Karate.run("classpath:com/example/features/orders") .tags("@orders") .outputHtmlReport(true); } @Karate.Test Karate testPerformance() { return Karate.run("classpath:com/example/features/performance") .tags("@performance") .outputJunitXml(true); } } // Parallel Test Runner package com.example.tests.runners; import com.intuit.karate.Results; import com.intuit.karate.Runner; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class ParallelRunner { @Test void testParallel() { Results results = Runner.path("classpath:com/example/features") .tags("~@ignore") .outputCucumberJson(true) .outputHtmlReport(true) .outputJunitXml(true) .reportDir("target/karate-reports") .parallel(5); assertEquals(0, results.getFailCount(), results.getErrorMessages()); } } 2. Java Helper Classes
package com.example.tests.helpers; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DriverManagerDataSource; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.UUID; public class JavaHelpers { private static final ObjectMapper objectMapper = new ObjectMapper(); public static String generateUniqueEmail() { String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); return "testuser_" + timestamp + "@example.com"; } public static String generateRandomString(int length) { return UUID.randomUUID().toString().replace("-", "").substring(0, length); } public static Map<String, Object> createUserPayload(String firstName, String lastName, String email) { ObjectNode payload = objectMapper.createObjectNode(); payload.put("firstName", firstName); payload.put("lastName", lastName); payload.put("email", email); payload.put("phone", "+1" + generateRandomString(10)); payload.put("dateOfBirth", "1990-01-01"); return objectMapper.convertValue(payload, Map.class); } public static Map<String, Object> createOrderPayload(String userId, List<Map<String, Object>> items) { ObjectNode payload = objectMapper.createObjectNode(); payload.put("userId", userId); payload.set("items", objectMapper.valueToTree(items)); ObjectNode address = objectMapper.createObjectNode(); address.put("street", "123 Test St"); address.put("city", "Test City"); address.put("state", "TC"); address.put("zipCode", "12345"); address.put("country", "Testland"); payload.set("shippingAddress", address); return objectMapper.convertValue(payload, Map.class); } public static boolean isValidEmail(String email) { return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$"); } public static void waitForCondition(int maxWaitSeconds, Runnable conditionCheck) { long startTime = System.currentTimeMillis(); long maxWaitMillis = maxWaitSeconds * 1000L; while (System.currentTimeMillis() - startTime < maxWaitMillis) { try { conditionCheck.run(); return; } catch (Exception e) { try { Thread.sleep(1000); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("Wait interrupted", ie); } } } throw new RuntimeException("Condition not met within " + maxWaitSeconds + " seconds"); } } package com.example.tests.helpers; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DriverManagerDataSource; import java.util.List; import java.util.Map; public class DatabaseUtils { private static JdbcTemplate jdbcTemplate; static { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("org.postgresql.Driver"); dataSource.setUrl("jdbc:postgresql://localhost:5432/testdb"); dataSource.setUsername("testuser"); dataSource.setPassword("testpass"); jdbcTemplate = new JdbcTemplate(dataSource); } public static void cleanupUser(String email) { jdbcTemplate.update("DELETE FROM users WHERE email = ?", email); } public static void cleanupOrder(String orderId) { jdbcTemplate.update("DELETE FROM order_items WHERE order_id = ?", orderId); jdbcTemplate.update("DELETE FROM orders WHERE id = ?", orderId); } public static Map<String, Object> getUserByEmail(String email) { List<Map<String, Object>> users = jdbcTemplate.queryForList( "SELECT * FROM users WHERE email = ?", email); return users.isEmpty() ? null : users.get(0); } public static Map<String, Object> getOrderById(String orderId) { List<Map<String, Object>> orders = jdbcTemplate.queryForList( "SELECT * FROM orders WHERE id = ?", orderId); return orders.isEmpty() ? null : orders.get(0); } public static void updateProductStock(String productId, int newStock) { jdbcTemplate.update("UPDATE products SET stock = ? WHERE id = ?", newStock, productId); } public static void resetDatabase() { jdbcTemplate.update("DELETE FROM order_items"); jdbcTemplate.update("DELETE FROM orders"); jdbcTemplate.update("DELETE FROM users"); jdbcTemplate.update("DELETE FROM products"); jdbcTemplate.update("ALTER SEQUENCE users_id_seq RESTART WITH 1"); jdbcTemplate.update("ALTER SEQUENCE orders_id_seq RESTART WITH 1"); } } 3. Configuration and Hooks
package com.example.tests.utils; import com.intuit.karate.RuntimeHook; import com.intuit.karate.core.Scenario; import com.intuit.karate.core.ScenarioResult; import com.intuit.karate.core.Feature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class KarateHooks implements RuntimeHook { private static final Logger logger = LoggerFactory.getLogger(KarateHooks.class); @Override public boolean beforeScenario(Scenario scenario) { logger.info("Starting scenario: {} in feature: {}", scenario.getName(), scenario.getFeature().getName()); return true; } @Override public void afterScenario(ScenarioResult result) { Scenario scenario = result.getScenario(); logger.info("Completed scenario: {} with status: {}", scenario.getName(), result.isFailed() ? "FAILED" : "PASSED"); if (result.isFailed()) { logger.error("Scenario failed with error: {}", result.getError()); } } @Override public boolean beforeFeature(Feature feature) { logger.info("Starting feature: {}", feature.getName()); return true; } @Override public void afterFeature(Feature feature) { logger.info("Completed feature: {}", feature.getName()); } } Advanced Karate Features
1. GraphQL Testing
# graphql/users-graphql.feature Feature: GraphQL User API Testing Background: * url baseUrl + '/graphql' * configure headers = { 'Content-Type': 'application/json' } Scenario: Query user by ID Given request """ { "query": "query GetUser($userId: ID!) { user(id: $userId) { id firstName lastName email } }", "variables": { "userId": "1" } } """ When method post Then status 200 And match response.data.user == """ { "id": "1", "firstName": "#string", "lastName": "#string", "email": "#string" } """ Scenario: Create user via GraphQL mutation Given def userData = { firstName: 'GraphQL', lastName: 'User', email: '[email protected]' } And request """ { "query": "mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { user { id firstName lastName email } } }", "variables": { "input": "#(userData)" } } """ When method post Then status 200 And match response.data.createUser.user.email == userData.email 2. Performance Testing
# performance/load-test.feature Feature: User API Load Testing Background: * url baseUrl * configure headers = { 'Content-Type': 'application/json' } @performance Scenario: Load test user creation endpoint * def startTime = function() { return java.lang.System.currentTimeMillis() } * def createUser = """ function() { var payload = { firstName: 'LoadTest', lastName: 'User' + karate.info.iteration, email: 'loadtest' + karate.info.iteration + '@example.com', phone: '+1234567890' }; karate.set('payload', payload); } """ Given path 'users' And request payload When method post Then status 201 * def endTime = function() { return java.lang.System.currentTimeMillis() } * def responseTime = endTime - startTime * print 'Response time: ' + responseTime + 'ms' @stress Scenario: Stress test with multiple concurrent users * configure retry = { count: 3, interval: 1000 } * call read('classpath:com/example/features/performance/stress-setup.feature') Given path 'users' And request { firstName: 'Stress', lastName: 'Test', email: '#(uniqueEmail)' } When method post Then status 201 3. Mock Server Testing
# mocks/external-service-mock.feature Feature: Mock External Service for Testing Background: * configure cors = true * def mockHelper = read('classpath:com/example/features/mocks/mock-helper.js') Scenario: mock user service Given path '/users' When method get Then status 200 And response { id: 1, name: 'Mock User', email: '[email protected]' } Scenario: mock user creation Given path '/users' And request { name: '#string', email: '#string' } When method post Then status 201 And def response = { id: '#(mockHelper.nextId())', name: '#(request.name)', email: '#(request.email)' } Scenario: mock with dynamic response Given path '/users', userId When method get Then """ if (userId == '1') { karate.set('response', { id: 1, name: 'User One' }); } else if (userId == '2') { karate.set('response', { id: 2, name: 'User Two' }); } else { karate.set('responseStatus', 404); karate.set('response', { error: 'User not found' }); } """ Configuration Files
1. Karate Configuration
// karate-config.js function fn() { var env = karate.env || 'dev'; karate.log('karate.env system property was:', env); var config = { env: env, baseUrl: 'http://localhost:8080/api/v1', authToken: null, requestTimeout: 30000, retryInterval: 1000, retryCount: 3 }; if (env == 'dev') { config.baseUrl = 'http://localhost:8080/api/v1'; config.dbUrl = 'jdbc:postgresql://localhost:5432/devdb'; } else if (env == 'qa') { config.baseUrl = 'https://qa-api.example.com/api/v1'; config.dbUrl = 'jdbc:postgresql://qa-db.example.com:5432/qadb'; } else if (env == 'staging') { config.baseUrl = 'https://staging-api.example.com/api/v1'; config.dbUrl = 'jdbc:postgresql://staging-db.example.com:5432/stagingdb'; } else if (env == 'prod') { config.baseUrl = 'https://api.example.com/api/v1'; // No direct DB access in prod } // Authentication setup if (env != 'dev') { var authResult = karate.callSingle('classpath:com/example/features/common/auth.feature', config); config.authToken = authResult.authToken; } // Configure headers for all requests karate.configure('headers', { 'Content-Type': 'application/json', 'User-Agent': 'Karate-Tests/1.0.0', 'X-Request-ID': '#(java.util.UUID.randomUUID().toString())' }); karate.configure('retry', { count: config.retryCount, interval: config.retryInterval }); karate.configure('connectTimeout', config.requestTimeout); karate.configure('readTimeout', config.requestTimeout); return config; } 2. JavaScript Utilities
// common/utils.js function generateRandomEmail() { var timestamp = new Date().getTime(); return 'test-' + timestamp + '@example.com'; } function generateRandomPhone() { return '+1' + Math.floor(1000000000 + Math.random() * 9000000000); } function validateResponseSchema(response, schema) { var matchResult = karate.match(response, schema); if (!matchResult.pass) { karate.fail('Schema validation failed: ' + matchResult.message); } } function waitForCondition(conditionFn, timeoutMs) { var startTime = new Date().getTime(); while (new Date().getTime() - startTime < timeoutMs) { if (conditionFn()) { return true; } java.lang.Thread.sleep(1000); } return false; } function parseJsonSafe(jsonString) { try { return JSON.parse(jsonString); } catch (e) { return null; } } CI/CD Integration
1. GitHub Actions
# .github/workflows/karate-tests.yml name: Karate API Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: api-tests: runs-on: ubuntu-latest services: postgres: image: postgres:13 env: POSTGRES_PASSWORD: test POSTGRES_USER: test POSTGRES_DB: testdb options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: 'maven' - name: Start Application run: | mvn spring-boot:run & echo $! > app.pid sleep 30 - name: Run Karate Tests run: | mvn test -Dtest=ParallelRunner -Dkarate.env=ci sleep 10 - name: Stop Application if: always() run: | kill $(cat app.pid) || true - name: Upload Test Reports if: always() uses: actions/upload-artifact@v4 with: name: karate-reports path: target/karate-reports/ - name: Publish Test Results if: always() uses: dorny/test-reporter@v1 with: name: Karate Test Results path: target/karate-reports/*.xml reporter: java-junit
Best Practices
1. Test Organization
# common/common.feature Feature: Common Utilities and Setup Background: * def JavaHelpers = Java.type('com.example.tests.helpers.JavaHelpers') * def DatabaseUtils = Java.type('com.example.tests.helpers.DatabaseUtils') * def generateEmail = function() { return JavaHelpers.generateUniqueEmail() } * def cleanupUser = function(email) { DatabaseUtils.cleanupUser(email) } @setup Scenario: Global setup for test suite * configure afterScenario = """ function() { var email = __arg.email || __arg.userData && __arg.userData.email; if (email) { karate.call('common.feature@cleanup', { email: email }); } } """ @cleanup Scenario: Cleanup test data * print 'Cleaning up test data for email:', email * eval if (email) DatabaseUtils.cleanupUser(email) @auth Scenario: Get authentication token Given url baseUrl And path 'auth', 'login' And request { username: '#(username)', password: '#(password)' } When method post Then status 200 And def authToken = response.token And match authToken != null 2. Error Handling and Retry
# common/retry.feature Feature: Retry and Error Handling Background: * configure retry = { count: 3, interval: 2000 } @retry Scenario: Retry flaky endpoint Given path 'flaky-endpoint' When method get Then status 200 And match response.status == 'READY' @errorhandling Scenario: Handle expected errors gracefully Given path 'users', 'invalid-id' When method get Then status 404 And match response == """ { "errorCode": "USER_NOT_FOUND", "message": "#string", "timestamp": "#notnull" } """ Scenario: Validate error response structure * def errorSchema = """ { "errorCode": "#string", "message": "#string", "timestamp": "#string", "details": "##[]" } """ Given path 'invalid-endpoint' When method get Then status 404 And match response == errorSchema Conclusion
Karate DSL provides a comprehensive solution for API testing with:
- Readable Syntax - BDD-style tests that are easy to understand
- Powerful Assertions - Rich matching capabilities with JSON schema validation
- Java Integration - Seamless integration with Java helpers and utilities
- Performance Testing - Built-in support for load and performance testing
- Mock Servers - Ability to create mocks for external dependencies
- CI/CD Ready - Easy integration with modern CI/CD pipelines
By following these patterns and best practices, teams can build maintainable, reliable, and comprehensive API test suites that scale with their applications.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.