From 668eae6b9bfd308b8947f5b6a1f42737f8c16f01 Mon Sep 17 00:00:00 2001 From: Qichao Chu Date: Tue, 26 May 2026 13:47:29 -0700 Subject: [PATCH] style: apply Google Java Format via Spotless --- .../uber/ugroup/ConsumerLagDetectionIT.java | 220 +++---- .../com/uber/ugroup/UGroupApplicationIT.java | 125 ++-- .../com/uber/ugroup/UGroupApplication.java | 10 +- .../com/uber/ugroup/api/HealthController.java | 93 ++- .../com/uber/ugroup/api/LagController.java | 224 +++---- .../ugroup/cache/CaffeineOffsetCache.java | 170 +++-- .../ugroup/cache/InMemoryOffsetCache.java | 91 ++- .../uber/ugroup/cache/NoopOffsetCache.java | 72 +-- .../com/uber/ugroup/cache/OffsetCache.java | 111 ++-- .../com/uber/ugroup/config/ClusterConfig.java | 106 ++-- .../config/EnvironmentClusterConfig.java | 108 ++-- .../config/UGroupAutoConfiguration.java | 431 ++++++------- .../uber/ugroup/config/UGroupProperties.java | 327 +++++----- .../uber/ugroup/config/YamlClusterConfig.java | 215 +++---- .../ugroup/fetcher/DirectOffsetFetcher.java | 289 +++++---- .../uber/ugroup/fetcher/OffsetFetcher.java | 119 ++-- .../uber/ugroup/metrics/MetricsProvider.java | 96 +-- .../metrics/MicrometerMetricsProvider.java | 115 ++-- .../ugroup/metrics/NoopMetricsProvider.java | 66 +- .../uber/ugroup/model/ConsumerLagState.java | 319 +++++----- .../uber/ugroup/model/ConsumerMetadata.java | 142 +++-- .../com/uber/ugroup/model/GroupAndTopic.java | 123 ++-- .../ugroup/model/LastCommittedOffset.java | 74 +-- .../BaseCompactedOffsetsProcessor.java | 329 +++++----- .../processor/CommittedOffsetsCompactor.java | 407 ++++++------ .../processor/CompactedOffsetsProcessor.java | 22 +- .../ConsumerOffsetTopicProcessor.java | 438 ++++++------- .../ugroup/processor/LagCalculationUtils.java | 96 ++- .../state/LastCommittedOffsetState.java | 92 ++- .../AllConsumersWatchListProvider.java | 72 +-- .../com/uber/ugroup/watchlist/BlockList.java | 264 ++++---- .../watchlist/RegexWatchListProvider.java | 173 +++-- .../watchlist/StaticWatchListProvider.java | 215 +++---- .../ugroup/watchlist/WatchListProvider.java | 98 ++- .../uber/ugroup/UGroupApplicationTest.java | 32 +- .../uber/ugroup/api/HealthControllerTest.java | 119 ++-- .../uber/ugroup/api/LagControllerTest.java | 382 ++++++----- .../ugroup/cache/CaffeineOffsetCacheTest.java | 165 +++-- .../ugroup/cache/InMemoryOffsetCacheTest.java | 345 +++++----- .../ugroup/cache/NoopOffsetCacheTest.java | 149 +++-- .../config/EnvironmentClusterConfigTest.java | 116 ++-- .../config/UGroupAutoConfigurationTest.java | 185 +++--- .../ugroup/config/UGroupPropertiesTest.java | 510 +++++++-------- .../ugroup/config/YamlClusterConfigTest.java | 218 ++++--- .../fetcher/DirectOffsetFetcherTest.java | 570 +++++++++-------- .../MicrometerMetricsProviderTest.java | 369 ++++++----- .../metrics/NoopMetricsProviderTest.java | 93 ++- .../ugroup/model/ConsumerLagStateTest.java | 596 +++++++++--------- .../ugroup/model/ConsumerMetadataTest.java | 83 ++- .../uber/ugroup/model/GroupAndTopicTest.java | 127 ++-- .../ugroup/model/LastCommittedOffsetTest.java | 123 ++-- .../BaseCompactedOffsetsProcessorTest.java | 589 +++++++++-------- .../CommittedOffsetsCompactorTest.java | 448 +++++++------ .../ConsumerOffsetTopicProcessorTest.java | 543 ++++++++-------- .../processor/LagCalculationUtilsTest.java | 118 ++-- .../state/LastCommittedOffsetStateTest.java | 271 ++++---- .../AllConsumersWatchListProviderTest.java | 151 +++-- .../uber/ugroup/watchlist/BlockListTest.java | 256 ++++---- .../watchlist/RegexWatchListProviderTest.java | 86 ++- .../StaticWatchListProviderTest.java | 316 +++++----- 60 files changed, 6354 insertions(+), 6458 deletions(-) diff --git a/src/integrationTest/java/com/uber/ugroup/ConsumerLagDetectionIT.java b/src/integrationTest/java/com/uber/ugroup/ConsumerLagDetectionIT.java index 871b478..41b13ed 100644 --- a/src/integrationTest/java/com/uber/ugroup/ConsumerLagDetectionIT.java +++ b/src/integrationTest/java/com/uber/ugroup/ConsumerLagDetectionIT.java @@ -15,6 +15,14 @@ */ package com.uber.ugroup; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.consumer.ConsumerConfig; @@ -40,129 +48,121 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - -/** - * Integration test that verifies consumer lag detection. - */ +/** Integration test that verifies consumer lag detection. */ @Testcontainers @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @org.springframework.test.annotation.DirtiesContext class ConsumerLagDetectionIT { - private static final String TEST_TOPIC = "test-lag-topic"; - private static final String TEST_GROUP = "test-consumer-group"; - private static final String KAFKA_IMAGE = System.getProperty("kafka.test.image", "confluentinc/cp-kafka:7.5.0"); + private static final String TEST_TOPIC = "test-lag-topic"; + private static final String TEST_GROUP = "test-consumer-group"; + private static final String KAFKA_IMAGE = + System.getProperty("kafka.test.image", "confluentinc/cp-kafka:7.5.0"); - @Container - static GenericContainer kafka = createKafkaContainer(); + @Container static GenericContainer kafka = createKafkaContainer(); - private static GenericContainer createKafkaContainer() { - if (KAFKA_IMAGE.startsWith("apache/kafka")) { - return new org.testcontainers.kafka.KafkaContainer(DockerImageName.parse(KAFKA_IMAGE)); - } - return new org.testcontainers.containers.KafkaContainer(DockerImageName.parse(KAFKA_IMAGE)); + private static GenericContainer createKafkaContainer() { + if (KAFKA_IMAGE.startsWith("apache/kafka")) { + return new org.testcontainers.kafka.KafkaContainer(DockerImageName.parse(KAFKA_IMAGE)); } + return new org.testcontainers.containers.KafkaContainer(DockerImageName.parse(KAFKA_IMAGE)); + } - private static String getBootstrapServers() { - if (kafka instanceof org.testcontainers.kafka.KafkaContainer kc) { - return kc.getBootstrapServers(); - } - return ((org.testcontainers.containers.KafkaContainer) kafka).getBootstrapServers(); + private static String getBootstrapServers() { + if (kafka instanceof org.testcontainers.kafka.KafkaContainer kc) { + return kc.getBootstrapServers(); } - - @LocalServerPort - private int port; - - @Autowired - private TestRestTemplate restTemplate; - - @DynamicPropertySource - static void kafkaProperties(DynamicPropertyRegistry registry) { - registry.add("ugroup.kafka.bootstrap-servers", ConsumerLagDetectionIT::getBootstrapServers); - registry.add("ugroup.kafka.cluster-name", () -> "test-cluster"); - registry.add("ugroup.watchlist.mode", () -> "all"); - registry.add("ugroup.processing.lag-report-interval-ms", () -> "1000"); - registry.add("ugroup.processing.compaction-interval-ms", () -> "1000"); + return ((org.testcontainers.containers.KafkaContainer) kafka).getBootstrapServers(); + } + + @LocalServerPort private int port; + + @Autowired private TestRestTemplate restTemplate; + + @DynamicPropertySource + static void kafkaProperties(DynamicPropertyRegistry registry) { + registry.add("ugroup.kafka.bootstrap-servers", ConsumerLagDetectionIT::getBootstrapServers); + registry.add("ugroup.kafka.cluster-name", () -> "test-cluster"); + registry.add("ugroup.watchlist.mode", () -> "all"); + registry.add("ugroup.processing.lag-report-interval-ms", () -> "1000"); + registry.add("ugroup.processing.compaction-interval-ms", () -> "1000"); + } + + @BeforeAll + static void setupKafka() throws Exception { + // Create test topic + Properties adminProps = new Properties(); + adminProps.put("bootstrap.servers", getBootstrapServers()); + + try (AdminClient admin = AdminClient.create(adminProps)) { + admin + .createTopics(List.of(new NewTopic(TEST_TOPIC, 3, (short) 1))) + .all() + .get(30, TimeUnit.SECONDS); } - @BeforeAll - static void setupKafka() throws Exception { - // Create test topic - Properties adminProps = new Properties(); - adminProps.put("bootstrap.servers", getBootstrapServers()); - - try (AdminClient admin = AdminClient.create(adminProps)) { - admin.createTopics(List.of(new NewTopic(TEST_TOPIC, 3, (short) 1))) - .all() - .get(30, TimeUnit.SECONDS); - } - - // Produce some messages - Properties producerProps = new Properties(); - producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, getBootstrapServers()); - producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); - producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); - - try (KafkaProducer producer = new KafkaProducer<>(producerProps)) { - for (int i = 0; i < 100; i++) { - producer.send(new ProducerRecord<>(TEST_TOPIC, "key-" + i, "value-" + i)); - } - producer.flush(); - } + // Produce some messages + Properties producerProps = new Properties(); + producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, getBootstrapServers()); + producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + producerProps.put( + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + + try (KafkaProducer producer = new KafkaProducer<>(producerProps)) { + for (int i = 0; i < 100; i++) { + producer.send(new ProducerRecord<>(TEST_TOPIC, "key-" + i, "value-" + i)); + } + producer.flush(); } - - @Test - void shouldDetectConsumerLag() throws Exception { - // Create a consumer and consume only partially - Properties consumerProps = new Properties(); - consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, getBootstrapServers()); - consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, TEST_GROUP); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); - consumerProps.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "10"); - - try (KafkaConsumer consumer = new KafkaConsumer<>(consumerProps)) { - consumer.subscribe(Collections.singletonList(TEST_TOPIC)); - - // Consume only 10 messages (leaving 90 behind) - ConsumerRecords records = consumer.poll(Duration.ofSeconds(10)); - assertThat(records.count()).isGreaterThan(0); - - // Commit the offsets - consumer.commitSync(); - } - - // Wait for uGroup to detect the lag - await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> { - ResponseEntity response = restTemplate.getForEntity( - "http://localhost:" + port + "/api/v1/lag/" + TEST_GROUP + "/" + TEST_TOPIC, - String.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).contains("totalLag"); - // Should have some lag since we only consumed 10 of 100 messages - }); + } + + @Test + void shouldDetectConsumerLag() throws Exception { + // Create a consumer and consume only partially + Properties consumerProps = new Properties(); + consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, getBootstrapServers()); + consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, TEST_GROUP); + consumerProps.put( + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + consumerProps.put( + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); + consumerProps.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "10"); + + try (KafkaConsumer consumer = new KafkaConsumer<>(consumerProps)) { + consumer.subscribe(Collections.singletonList(TEST_TOPIC)); + + // Consume only 10 messages (leaving 90 behind) + ConsumerRecords records = consumer.poll(Duration.ofSeconds(10)); + assertThat(records.count()).isGreaterThan(0); + + // Commit the offsets + consumer.commitSync(); } - @Test - void shouldReturnNotFoundForUnknownGroup() { - ResponseEntity response = restTemplate.getForEntity( - "http://localhost:" + port + "/api/v1/lag/nonexistent-group", - String.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - } + // Wait for uGroup to detect the lag + await() + .atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + ResponseEntity response = + restTemplate.getForEntity( + "http://localhost:" + port + "/api/v1/lag/" + TEST_GROUP + "/" + TEST_TOPIC, + String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("totalLag"); + // Should have some lag since we only consumed 10 of 100 messages + }); + } + + @Test + void shouldReturnNotFoundForUnknownGroup() { + ResponseEntity response = + restTemplate.getForEntity( + "http://localhost:" + port + "/api/v1/lag/nonexistent-group", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } } diff --git a/src/integrationTest/java/com/uber/ugroup/UGroupApplicationIT.java b/src/integrationTest/java/com/uber/ugroup/UGroupApplicationIT.java index 797752f..46619f3 100644 --- a/src/integrationTest/java/com/uber/ugroup/UGroupApplicationIT.java +++ b/src/integrationTest/java/com/uber/ugroup/UGroupApplicationIT.java @@ -15,6 +15,8 @@ */ package com.uber.ugroup; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -29,80 +31,71 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration test that verifies uGroup can start and connect to Kafka. - */ +/** Integration test that verifies uGroup can start and connect to Kafka. */ @Testcontainers @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @org.springframework.test.annotation.DirtiesContext class UGroupApplicationIT { - private static final String KAFKA_IMAGE = System.getProperty("kafka.test.image", "confluentinc/cp-kafka:7.5.0"); - - @Container - static GenericContainer kafka = createKafkaContainer(); - - private static GenericContainer createKafkaContainer() { - if (KAFKA_IMAGE.startsWith("apache/kafka")) { - return new org.testcontainers.kafka.KafkaContainer(DockerImageName.parse(KAFKA_IMAGE)); - } - return new org.testcontainers.containers.KafkaContainer(DockerImageName.parse(KAFKA_IMAGE)); - } - - private static String getBootstrapServers() { - if (kafka instanceof org.testcontainers.kafka.KafkaContainer kc) { - return kc.getBootstrapServers(); - } - return ((org.testcontainers.containers.KafkaContainer) kafka).getBootstrapServers(); - } - - @LocalServerPort - private int port; - - @Autowired - private TestRestTemplate restTemplate; - - @DynamicPropertySource - static void kafkaProperties(DynamicPropertyRegistry registry) { - registry.add("ugroup.kafka.bootstrap-servers", UGroupApplicationIT::getBootstrapServers); - registry.add("ugroup.kafka.cluster-name", () -> "test-cluster"); - registry.add("ugroup.watchlist.mode", () -> "all"); - } - - @Test - void contextLoads() { - // Application should start successfully - } + private static final String KAFKA_IMAGE = + System.getProperty("kafka.test.image", "confluentinc/cp-kafka:7.5.0"); - @Test - void healthEndpoint_returnsUp() { - ResponseEntity response = restTemplate.getForEntity( - "http://localhost:" + port + "/actuator/health", - String.class); + @Container static GenericContainer kafka = createKafkaContainer(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).contains("UP"); + private static GenericContainer createKafkaContainer() { + if (KAFKA_IMAGE.startsWith("apache/kafka")) { + return new org.testcontainers.kafka.KafkaContainer(DockerImageName.parse(KAFKA_IMAGE)); } + return new org.testcontainers.containers.KafkaContainer(DockerImageName.parse(KAFKA_IMAGE)); + } - @Test - void statusEndpoint_returnsStatus() { - ResponseEntity response = restTemplate.getForEntity( - "http://localhost:" + port + "/api/v1/status", - String.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).contains("running"); - assertThat(response.getBody()).contains("test-cluster"); - } - - @Test - void metricsEndpoint_available() { - ResponseEntity response = restTemplate.getForEntity( - "http://localhost:" + port + "/actuator/metrics", - String.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + private static String getBootstrapServers() { + if (kafka instanceof org.testcontainers.kafka.KafkaContainer kc) { + return kc.getBootstrapServers(); } + return ((org.testcontainers.containers.KafkaContainer) kafka).getBootstrapServers(); + } + + @LocalServerPort private int port; + + @Autowired private TestRestTemplate restTemplate; + + @DynamicPropertySource + static void kafkaProperties(DynamicPropertyRegistry registry) { + registry.add("ugroup.kafka.bootstrap-servers", UGroupApplicationIT::getBootstrapServers); + registry.add("ugroup.kafka.cluster-name", () -> "test-cluster"); + registry.add("ugroup.watchlist.mode", () -> "all"); + } + + @Test + void contextLoads() { + // Application should start successfully + } + + @Test + void healthEndpoint_returnsUp() { + ResponseEntity response = + restTemplate.getForEntity("http://localhost:" + port + "/actuator/health", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("UP"); + } + + @Test + void statusEndpoint_returnsStatus() { + ResponseEntity response = + restTemplate.getForEntity("http://localhost:" + port + "/api/v1/status", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("running"); + assertThat(response.getBody()).contains("test-cluster"); + } + + @Test + void metricsEndpoint_available() { + ResponseEntity response = + restTemplate.getForEntity("http://localhost:" + port + "/actuator/metrics", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } } diff --git a/src/main/java/com/uber/ugroup/UGroupApplication.java b/src/main/java/com/uber/ugroup/UGroupApplication.java index 3203a1d..9e4a390 100644 --- a/src/main/java/com/uber/ugroup/UGroupApplication.java +++ b/src/main/java/com/uber/ugroup/UGroupApplication.java @@ -21,13 +21,13 @@ /** * Main entry point for uGroup application. * - * uGroup monitors Kafka consumer groups by reading the __consumer_offsets topic - * and calculating/reporting consumer lag metrics. + *

uGroup monitors Kafka consumer groups by reading the __consumer_offsets topic and + * calculating/reporting consumer lag metrics. */ @SpringBootApplication public class UGroupApplication { - public static void main(String[] args) { - SpringApplication.run(UGroupApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(UGroupApplication.class, args); + } } diff --git a/src/main/java/com/uber/ugroup/api/HealthController.java b/src/main/java/com/uber/ugroup/api/HealthController.java index c145f44..077fbd3 100644 --- a/src/main/java/com/uber/ugroup/api/HealthController.java +++ b/src/main/java/com/uber/ugroup/api/HealthController.java @@ -17,6 +17,8 @@ import com.uber.ugroup.cache.OffsetCache; import com.uber.ugroup.config.UGroupProperties; +import java.time.Duration; +import java.time.Instant; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.http.ResponseEntity; @@ -24,63 +26,56 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.lang.management.ManagementFactory; -import java.time.Duration; -import java.time.Instant; -import java.util.Map; - -/** - * Health and status endpoints. - */ +/** Health and status endpoints. */ @RestController @RequestMapping("/api/v1") public class HealthController implements HealthIndicator { - private final UGroupProperties properties; - private final OffsetCache offsetCache; - private final Instant startTime; + private final UGroupProperties properties; + private final OffsetCache offsetCache; + private final Instant startTime; - public HealthController(UGroupProperties properties, OffsetCache offsetCache) { - this.properties = properties; - this.offsetCache = offsetCache; - this.startTime = Instant.now(); - } + public HealthController(UGroupProperties properties, OffsetCache offsetCache) { + this.properties = properties; + this.offsetCache = offsetCache; + this.startTime = Instant.now(); + } - @GetMapping("/status") - public ResponseEntity getStatus() { - long uptimeMs = Duration.between(startTime, Instant.now()).toMillis(); + @GetMapping("/status") + public ResponseEntity getStatus() { + long uptimeMs = Duration.between(startTime, Instant.now()).toMillis(); - return ResponseEntity.ok(new StatusResponse( - "running", - uptimeMs, - properties.getKafka().getClusterName(), - properties.getKafka().getBootstrapServers(), - offsetCache.size(), - Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), - Runtime.getRuntime().maxMemory())); - } + return ResponseEntity.ok( + new StatusResponse( + "running", + uptimeMs, + properties.getKafka().getClusterName(), + properties.getKafka().getBootstrapServers(), + offsetCache.size(), + Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), + Runtime.getRuntime().maxMemory())); + } - @Override - public Health health() { - try { - // Basic health check - return Health.up() - .withDetail("cluster", properties.getKafka().getClusterName()) - .withDetail("cacheSize", offsetCache.size()) - .withDetail("uptime", Duration.between(startTime, Instant.now()).toString()) - .build(); - } catch (Exception e) { - return Health.down(e).build(); - } + @Override + public Health health() { + try { + // Basic health check + return Health.up() + .withDetail("cluster", properties.getKafka().getClusterName()) + .withDetail("cacheSize", offsetCache.size()) + .withDetail("uptime", Duration.between(startTime, Instant.now()).toString()) + .build(); + } catch (Exception e) { + return Health.down(e).build(); } + } - public record StatusResponse( - String status, - long uptimeMs, - String cluster, - String bootstrapServers, - long cacheSize, - long usedMemory, - long maxMemory) { - } + public record StatusResponse( + String status, + long uptimeMs, + String cluster, + String bootstrapServers, + long cacheSize, + long usedMemory, + long maxMemory) {} } diff --git a/src/main/java/com/uber/ugroup/api/LagController.java b/src/main/java/com/uber/ugroup/api/LagController.java index 4e2015b..aba8475 100644 --- a/src/main/java/com/uber/ugroup/api/LagController.java +++ b/src/main/java/com/uber/ugroup/api/LagController.java @@ -17,7 +17,9 @@ import com.uber.ugroup.config.UGroupProperties; import com.uber.ugroup.fetcher.OffsetFetcher; -import com.uber.ugroup.model.GroupAndTopic; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; @@ -27,145 +29,115 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * REST API for querying consumer group lag. - */ +/** REST API for querying consumer group lag. */ @RestController @RequestMapping("/api/v1") public class LagController { - private final OffsetFetcher offsetFetcher; - private final UGroupProperties properties; + private final OffsetFetcher offsetFetcher; + private final UGroupProperties properties; - public LagController(OffsetFetcher offsetFetcher, UGroupProperties properties) { - this.offsetFetcher = offsetFetcher; - this.properties = properties; - } + public LagController(OffsetFetcher offsetFetcher, UGroupProperties properties) { + this.offsetFetcher = offsetFetcher; + this.properties = properties; + } - /** - * Get lag for a specific consumer group and topic. - */ - @GetMapping("/lag/{group}/{topic}") - public ResponseEntity getLag( - @PathVariable String group, - @PathVariable String topic) { - - List partitions = offsetFetcher.partitionsFor(topic); - if (partitions == null || partitions.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - List topicPartitions = partitions.stream() - .map(pi -> new TopicPartition(topic, pi.partition())) - .toList(); - - Map beginningOffsets = offsetFetcher.beginningOffsets(topicPartitions); - Map endOffsets = offsetFetcher.endOffsets(topicPartitions); - Map committedOffsets = offsetFetcher.listConsumerGroupOffsets(group); - - Map partitionLags = new HashMap<>(); - long totalLag = 0; - - for (TopicPartition tp : topicPartitions) { - if (!tp.topic().equals(topic)) continue; - - long beginning = beginningOffsets.getOrDefault(tp, 0L); - long end = endOffsets.getOrDefault(tp, 0L); - OffsetAndMetadata committed = committedOffsets.get(tp); - long committedOffset = committed != null ? committed.offset() : -1; - - long lag = committedOffset >= 0 ? Math.max(0, end - committedOffset) : end - beginning; - totalLag += lag; - - partitionLags.put(tp.partition(), new PartitionLag( - tp.partition(), - beginning, - committedOffset, - end, - lag)); - } - - LagResponse response = new LagResponse( - group, - topic, - properties.getKafka().getClusterName(), - totalLag, - partitionLags); - - return ResponseEntity.ok(response); + /** Get lag for a specific consumer group and topic. */ + @GetMapping("/lag/{group}/{topic}") + public ResponseEntity getLag( + @PathVariable String group, @PathVariable String topic) { + + List partitions = offsetFetcher.partitionsFor(topic); + if (partitions == null || partitions.isEmpty()) { + return ResponseEntity.notFound().build(); } - /** - * Get lag summary for a consumer group across all topics. - */ - @GetMapping("/lag/{group}") - public ResponseEntity getGroupLag(@PathVariable String group) { - Map committedOffsets = offsetFetcher.listConsumerGroupOffsets(group); - - if (committedOffsets.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - Map topicLags = new HashMap<>(); - long totalLag = 0; - - // Group by topic - Map> byTopic = new HashMap<>(); - for (TopicPartition tp : committedOffsets.keySet()) { - byTopic.computeIfAbsent(tp.topic(), k -> new java.util.ArrayList<>()).add(tp); - } - - for (Map.Entry> entry : byTopic.entrySet()) { - String topic = entry.getKey(); - List tps = entry.getValue(); - - Map endOffsets = offsetFetcher.endOffsets(tps); - - long topicLag = 0; - for (TopicPartition tp : tps) { - long end = endOffsets.getOrDefault(tp, 0L); - OffsetAndMetadata committed = committedOffsets.get(tp); - long committedOffset = committed != null ? committed.offset() : 0; - topicLag += Math.max(0, end - committedOffset); - } - - topicLags.put(topic, topicLag); - totalLag += topicLag; - } - - return ResponseEntity.ok(new GroupLagSummary( - group, - properties.getKafka().getClusterName(), - totalLag, - topicLags)); + List topicPartitions = + partitions.stream().map(pi -> new TopicPartition(topic, pi.partition())).toList(); + + Map beginningOffsets = offsetFetcher.beginningOffsets(topicPartitions); + Map endOffsets = offsetFetcher.endOffsets(topicPartitions); + Map committedOffsets = + offsetFetcher.listConsumerGroupOffsets(group); + + Map partitionLags = new HashMap<>(); + long totalLag = 0; + + for (TopicPartition tp : topicPartitions) { + if (!tp.topic().equals(topic)) continue; + + long beginning = beginningOffsets.getOrDefault(tp, 0L); + long end = endOffsets.getOrDefault(tp, 0L); + OffsetAndMetadata committed = committedOffsets.get(tp); + long committedOffset = committed != null ? committed.offset() : -1; + + long lag = committedOffset >= 0 ? Math.max(0, end - committedOffset) : end - beginning; + totalLag += lag; + + partitionLags.put( + tp.partition(), new PartitionLag(tp.partition(), beginning, committedOffset, end, lag)); } - // Response DTOs + LagResponse response = + new LagResponse( + group, topic, properties.getKafka().getClusterName(), totalLag, partitionLags); + + return ResponseEntity.ok(response); + } + + /** Get lag summary for a consumer group across all topics. */ + @GetMapping("/lag/{group}") + public ResponseEntity getGroupLag(@PathVariable String group) { + Map committedOffsets = + offsetFetcher.listConsumerGroupOffsets(group); - public record LagResponse( - String group, - String topic, - String cluster, - long totalLag, - Map partitions) { + if (committedOffsets.isEmpty()) { + return ResponseEntity.notFound().build(); } - public record PartitionLag( - int partition, - long beginningOffset, - long committedOffset, - long endOffset, - long lag) { + Map topicLags = new HashMap<>(); + long totalLag = 0; + + // Group by topic + Map> byTopic = new HashMap<>(); + for (TopicPartition tp : committedOffsets.keySet()) { + byTopic.computeIfAbsent(tp.topic(), k -> new java.util.ArrayList<>()).add(tp); } - public record GroupLagSummary( - String group, - String cluster, - long totalLag, - Map topicLags) { + for (Map.Entry> entry : byTopic.entrySet()) { + String topic = entry.getKey(); + List tps = entry.getValue(); + + Map endOffsets = offsetFetcher.endOffsets(tps); + + long topicLag = 0; + for (TopicPartition tp : tps) { + long end = endOffsets.getOrDefault(tp, 0L); + OffsetAndMetadata committed = committedOffsets.get(tp); + long committedOffset = committed != null ? committed.offset() : 0; + topicLag += Math.max(0, end - committedOffset); + } + + topicLags.put(topic, topicLag); + totalLag += topicLag; } + + return ResponseEntity.ok( + new GroupLagSummary(group, properties.getKafka().getClusterName(), totalLag, topicLags)); + } + + // Response DTOs + + public record LagResponse( + String group, + String topic, + String cluster, + long totalLag, + Map partitions) {} + + public record PartitionLag( + int partition, long beginningOffset, long committedOffset, long endOffset, long lag) {} + + public record GroupLagSummary( + String group, String cluster, long totalLag, Map topicLags) {} } diff --git a/src/main/java/com/uber/ugroup/cache/CaffeineOffsetCache.java b/src/main/java/com/uber/ugroup/cache/CaffeineOffsetCache.java index 4a4eb09..e69b07d 100644 --- a/src/main/java/com/uber/ugroup/cache/CaffeineOffsetCache.java +++ b/src/main/java/com/uber/ugroup/cache/CaffeineOffsetCache.java @@ -19,98 +19,94 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.uber.ugroup.model.GroupAndTopic; import com.uber.ugroup.model.LastCommittedOffset; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.time.Duration; import java.util.Map; import java.util.Optional; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -/** - * OffsetCache implementation using Caffeine for high-performance in-memory caching. - */ +/** OffsetCache implementation using Caffeine for high-performance in-memory caching. */ public class CaffeineOffsetCache implements OffsetCache { - private static final Logger logger = LoggerFactory.getLogger(CaffeineOffsetCache.class); - - private final Cache> offsetCache; - private final Cache timestampIndexCache; - - public CaffeineOffsetCache() { - this(10000, Duration.ofMinutes(5)); - } - - public CaffeineOffsetCache(int maxSize, Duration ttl) { - this.offsetCache = Caffeine.newBuilder() - .maximumSize(maxSize) - .expireAfterWrite(ttl) - .recordStats() - .build(); - - this.timestampIndexCache = Caffeine.newBuilder() - .maximumSize(maxSize * 10) // More entries for timestamp index - .expireAfterWrite(Duration.ofHours(1)) - .build(); - - logger.info("Created CaffeineOffsetCache with maxSize={}, ttl={}", maxSize, ttl); - } - - @Override - public void put(GroupAndTopic groupAndTopic, Map offsets) { - offsetCache.put(groupAndTopic, Map.copyOf(offsets)); - } - - @Override - public Optional> get(GroupAndTopic groupAndTopic) { - return Optional.ofNullable(offsetCache.getIfPresent(groupAndTopic)); - } - - @Override - public void invalidate(GroupAndTopic groupAndTopic) { - offsetCache.invalidate(groupAndTopic); - } - - @Override - public void invalidateExcept(Set retainedPartitions) { - // Remove entries for groups whose consumer_offsets partition is not in retainedPartitions - offsetCache.asMap().keySet().removeIf( - gat -> !retainedPartitions.contains(gat.getConsumerOffsetsPartition())); - } - - @Override - public void clear() { - offsetCache.invalidateAll(); - timestampIndexCache.invalidateAll(); - } - - @Override - public long size() { - return offsetCache.estimatedSize(); - } - - @Override - public void putOffsetIndex(String topic, int partition, long offset, long timestamp) { - String key = buildTimestampKey(topic, partition, offset); - timestampIndexCache.put(key, timestamp); - } - - @Override - public Optional getTimestampForOffset(String topic, int partition, long offset) { - String key = buildTimestampKey(topic, partition, offset); - return Optional.ofNullable(timestampIndexCache.getIfPresent(key)); - } - - private String buildTimestampKey(String topic, int partition, long offset) { - return topic + ":" + partition + ":" + offset; - } - - /** - * Get cache statistics for monitoring. - */ - public String getStats() { - var stats = offsetCache.stats(); - return String.format("hits=%d, misses=%d, hitRate=%.2f%%, size=%d", - stats.hitCount(), stats.missCount(), stats.hitRate() * 100, offsetCache.estimatedSize()); - } + private static final Logger logger = LoggerFactory.getLogger(CaffeineOffsetCache.class); + + private final Cache> offsetCache; + private final Cache timestampIndexCache; + + public CaffeineOffsetCache() { + this(10000, Duration.ofMinutes(5)); + } + + public CaffeineOffsetCache(int maxSize, Duration ttl) { + this.offsetCache = + Caffeine.newBuilder().maximumSize(maxSize).expireAfterWrite(ttl).recordStats().build(); + + this.timestampIndexCache = + Caffeine.newBuilder() + .maximumSize(maxSize * 10) // More entries for timestamp index + .expireAfterWrite(Duration.ofHours(1)) + .build(); + + logger.info("Created CaffeineOffsetCache with maxSize={}, ttl={}", maxSize, ttl); + } + + @Override + public void put(GroupAndTopic groupAndTopic, Map offsets) { + offsetCache.put(groupAndTopic, Map.copyOf(offsets)); + } + + @Override + public Optional> get(GroupAndTopic groupAndTopic) { + return Optional.ofNullable(offsetCache.getIfPresent(groupAndTopic)); + } + + @Override + public void invalidate(GroupAndTopic groupAndTopic) { + offsetCache.invalidate(groupAndTopic); + } + + @Override + public void invalidateExcept(Set retainedPartitions) { + // Remove entries for groups whose consumer_offsets partition is not in retainedPartitions + offsetCache + .asMap() + .keySet() + .removeIf(gat -> !retainedPartitions.contains(gat.getConsumerOffsetsPartition())); + } + + @Override + public void clear() { + offsetCache.invalidateAll(); + timestampIndexCache.invalidateAll(); + } + + @Override + public long size() { + return offsetCache.estimatedSize(); + } + + @Override + public void putOffsetIndex(String topic, int partition, long offset, long timestamp) { + String key = buildTimestampKey(topic, partition, offset); + timestampIndexCache.put(key, timestamp); + } + + @Override + public Optional getTimestampForOffset(String topic, int partition, long offset) { + String key = buildTimestampKey(topic, partition, offset); + return Optional.ofNullable(timestampIndexCache.getIfPresent(key)); + } + + private String buildTimestampKey(String topic, int partition, long offset) { + return topic + ":" + partition + ":" + offset; + } + + /** Get cache statistics for monitoring. */ + public String getStats() { + var stats = offsetCache.stats(); + return String.format( + "hits=%d, misses=%d, hitRate=%.2f%%, size=%d", + stats.hitCount(), stats.missCount(), stats.hitRate() * 100, offsetCache.estimatedSize()); + } } diff --git a/src/main/java/com/uber/ugroup/cache/InMemoryOffsetCache.java b/src/main/java/com/uber/ugroup/cache/InMemoryOffsetCache.java index 3716dd8..5cf8ab4 100644 --- a/src/main/java/com/uber/ugroup/cache/InMemoryOffsetCache.java +++ b/src/main/java/com/uber/ugroup/cache/InMemoryOffsetCache.java @@ -17,69 +17,68 @@ import com.uber.ugroup.model.GroupAndTopic; import com.uber.ugroup.model.LastCommittedOffset; - import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** - * Simple in-memory OffsetCache using ConcurrentHashMap. - * Suitable for testing or small deployments. + * Simple in-memory OffsetCache using ConcurrentHashMap. Suitable for testing or small deployments. */ public class InMemoryOffsetCache implements OffsetCache { - private final ConcurrentHashMap> offsetCache; - private final ConcurrentHashMap timestampIndex; + private final ConcurrentHashMap> offsetCache; + private final ConcurrentHashMap timestampIndex; - public InMemoryOffsetCache() { - this.offsetCache = new ConcurrentHashMap<>(); - this.timestampIndex = new ConcurrentHashMap<>(); - } + public InMemoryOffsetCache() { + this.offsetCache = new ConcurrentHashMap<>(); + this.timestampIndex = new ConcurrentHashMap<>(); + } - @Override - public void put(GroupAndTopic groupAndTopic, Map offsets) { - offsetCache.put(groupAndTopic, Map.copyOf(offsets)); - } + @Override + public void put(GroupAndTopic groupAndTopic, Map offsets) { + offsetCache.put(groupAndTopic, Map.copyOf(offsets)); + } - @Override - public Optional> get(GroupAndTopic groupAndTopic) { - return Optional.ofNullable(offsetCache.get(groupAndTopic)); - } + @Override + public Optional> get(GroupAndTopic groupAndTopic) { + return Optional.ofNullable(offsetCache.get(groupAndTopic)); + } - @Override - public void invalidate(GroupAndTopic groupAndTopic) { - offsetCache.remove(groupAndTopic); - } + @Override + public void invalidate(GroupAndTopic groupAndTopic) { + offsetCache.remove(groupAndTopic); + } - @Override - public void invalidateExcept(Set retainedPartitions) { - offsetCache.keySet().removeIf( - gat -> !retainedPartitions.contains(gat.getConsumerOffsetsPartition())); - } + @Override + public void invalidateExcept(Set retainedPartitions) { + offsetCache + .keySet() + .removeIf(gat -> !retainedPartitions.contains(gat.getConsumerOffsetsPartition())); + } - @Override - public void clear() { - offsetCache.clear(); - timestampIndex.clear(); - } + @Override + public void clear() { + offsetCache.clear(); + timestampIndex.clear(); + } - @Override - public long size() { - return offsetCache.size(); - } + @Override + public long size() { + return offsetCache.size(); + } - @Override - public void putOffsetIndex(String topic, int partition, long offset, long timestamp) { - timestampIndex.put(buildKey(topic, partition, offset), timestamp); - } + @Override + public void putOffsetIndex(String topic, int partition, long offset, long timestamp) { + timestampIndex.put(buildKey(topic, partition, offset), timestamp); + } - @Override - public Optional getTimestampForOffset(String topic, int partition, long offset) { - return Optional.ofNullable(timestampIndex.get(buildKey(topic, partition, offset))); - } + @Override + public Optional getTimestampForOffset(String topic, int partition, long offset) { + return Optional.ofNullable(timestampIndex.get(buildKey(topic, partition, offset))); + } - private String buildKey(String topic, int partition, long offset) { - return topic + ":" + partition + ":" + offset; - } + private String buildKey(String topic, int partition, long offset) { + return topic + ":" + partition + ":" + offset; + } } diff --git a/src/main/java/com/uber/ugroup/cache/NoopOffsetCache.java b/src/main/java/com/uber/ugroup/cache/NoopOffsetCache.java index c1d3260..f05bbc9 100644 --- a/src/main/java/com/uber/ugroup/cache/NoopOffsetCache.java +++ b/src/main/java/com/uber/ugroup/cache/NoopOffsetCache.java @@ -17,56 +17,52 @@ import com.uber.ugroup.model.GroupAndTopic; import com.uber.ugroup.model.LastCommittedOffset; - import java.util.Map; import java.util.Optional; import java.util.Set; -/** - * No-op OffsetCache implementation. - * Use when caching is disabled or not needed. - */ +/** No-op OffsetCache implementation. Use when caching is disabled or not needed. */ public class NoopOffsetCache implements OffsetCache { - public static final NoopOffsetCache INSTANCE = new NoopOffsetCache(); + public static final NoopOffsetCache INSTANCE = new NoopOffsetCache(); - @Override - public void put(GroupAndTopic groupAndTopic, Map offsets) { - // no-op - } + @Override + public void put(GroupAndTopic groupAndTopic, Map offsets) { + // no-op + } - @Override - public Optional> get(GroupAndTopic groupAndTopic) { - return Optional.empty(); - } + @Override + public Optional> get(GroupAndTopic groupAndTopic) { + return Optional.empty(); + } - @Override - public void invalidate(GroupAndTopic groupAndTopic) { - // no-op - } + @Override + public void invalidate(GroupAndTopic groupAndTopic) { + // no-op + } - @Override - public void invalidateExcept(Set retainedPartitions) { - // no-op - } + @Override + public void invalidateExcept(Set retainedPartitions) { + // no-op + } - @Override - public void clear() { - // no-op - } + @Override + public void clear() { + // no-op + } - @Override - public long size() { - return 0; - } + @Override + public long size() { + return 0; + } - @Override - public void putOffsetIndex(String topic, int partition, long offset, long timestamp) { - // no-op - } + @Override + public void putOffsetIndex(String topic, int partition, long offset, long timestamp) { + // no-op + } - @Override - public Optional getTimestampForOffset(String topic, int partition, long offset) { - return Optional.empty(); - } + @Override + public Optional getTimestampForOffset(String topic, int partition, long offset) { + return Optional.empty(); + } } diff --git a/src/main/java/com/uber/ugroup/cache/OffsetCache.java b/src/main/java/com/uber/ugroup/cache/OffsetCache.java index e425916..818c482 100644 --- a/src/main/java/com/uber/ugroup/cache/OffsetCache.java +++ b/src/main/java/com/uber/ugroup/cache/OffsetCache.java @@ -17,75 +17,72 @@ import com.uber.ugroup.model.GroupAndTopic; import com.uber.ugroup.model.LastCommittedOffset; - import java.util.Map; import java.util.Optional; /** - * Interface for caching offset data. Allows pluggable cache implementations - * (e.g., in-memory, Redis, Caffeine). + * Interface for caching offset data. Allows pluggable cache implementations (e.g., in-memory, + * Redis, Caffeine). */ public interface OffsetCache { - /** - * Store the last committed offsets for a group-topic combination. - * - * @param groupAndTopic the group and topic - * @param offsets map of partition to last committed offset - */ - void put(GroupAndTopic groupAndTopic, Map offsets); + /** + * Store the last committed offsets for a group-topic combination. + * + * @param groupAndTopic the group and topic + * @param offsets map of partition to last committed offset + */ + void put(GroupAndTopic groupAndTopic, Map offsets); - /** - * Get the cached offsets for a group-topic combination. - * - * @param groupAndTopic the group and topic - * @return optional map of partition to offset if cached - */ - Optional> get(GroupAndTopic groupAndTopic); + /** + * Get the cached offsets for a group-topic combination. + * + * @param groupAndTopic the group and topic + * @return optional map of partition to offset if cached + */ + Optional> get(GroupAndTopic groupAndTopic); - /** - * Remove cached data for a group-topic combination. - * - * @param groupAndTopic the group and topic to invalidate - */ - void invalidate(GroupAndTopic groupAndTopic); + /** + * Remove cached data for a group-topic combination. + * + * @param groupAndTopic the group and topic to invalidate + */ + void invalidate(GroupAndTopic groupAndTopic); - /** - * Remove all cached data for partitions of __consumer_offsets that are no longer assigned. - * - * @param retainedPartitions partitions that should be retained - */ - void invalidateExcept(java.util.Set retainedPartitions); + /** + * Remove all cached data for partitions of __consumer_offsets that are no longer assigned. + * + * @param retainedPartitions partitions that should be retained + */ + void invalidateExcept(java.util.Set retainedPartitions); - /** - * Clear all cached data. - */ - void clear(); + /** Clear all cached data. */ + void clear(); - /** - * Get the current size of the cache. - * - * @return number of cached entries - */ - long size(); + /** + * Get the current size of the cache. + * + * @return number of cached entries + */ + long size(); - /** - * Store an offset-to-timestamp index entry for time-based lag calculation. - * - * @param topic the topic name - * @param partition the partition number - * @param offset the offset - * @param timestamp the timestamp at that offset - */ - void putOffsetIndex(String topic, int partition, long offset, long timestamp); + /** + * Store an offset-to-timestamp index entry for time-based lag calculation. + * + * @param topic the topic name + * @param partition the partition number + * @param offset the offset + * @param timestamp the timestamp at that offset + */ + void putOffsetIndex(String topic, int partition, long offset, long timestamp); - /** - * Get timestamp for a given offset (for time-based lag calculation). - * - * @param topic the topic name - * @param partition the partition number - * @param offset the offset - * @return optional timestamp if cached - */ - Optional getTimestampForOffset(String topic, int partition, long offset); + /** + * Get timestamp for a given offset (for time-based lag calculation). + * + * @param topic the topic name + * @param partition the partition number + * @param offset the offset + * @return optional timestamp if cached + */ + Optional getTimestampForOffset(String topic, int partition, long offset); } diff --git a/src/main/java/com/uber/ugroup/config/ClusterConfig.java b/src/main/java/com/uber/ugroup/config/ClusterConfig.java index b931c33..e8cba84 100644 --- a/src/main/java/com/uber/ugroup/config/ClusterConfig.java +++ b/src/main/java/com/uber/ugroup/config/ClusterConfig.java @@ -20,67 +20,67 @@ import java.util.Set; /** - * Interface for providing Kafka cluster configuration. - * Allows pluggable cluster discovery mechanisms. + * Interface for providing Kafka cluster configuration. Allows pluggable cluster discovery + * mechanisms. */ public interface ClusterConfig { - /** - * Get the name/identifier of this cluster. - * - * @return the cluster name - */ - String getClusterName(); + /** + * Get the name/identifier of this cluster. + * + * @return the cluster name + */ + String getClusterName(); - /** - * Get the Kafka bootstrap servers for this cluster. - * - * @return comma-separated list of bootstrap servers - */ - String getBootstrapServers(); + /** + * Get the Kafka bootstrap servers for this cluster. + * + * @return comma-separated list of bootstrap servers + */ + String getBootstrapServers(); - /** - * Get additional Kafka consumer properties for this cluster. - * - * @return Kafka consumer properties - */ - Properties getConsumerProperties(); + /** + * Get additional Kafka consumer properties for this cluster. + * + * @return Kafka consumer properties + */ + Properties getConsumerProperties(); - /** - * Get additional Kafka admin client properties for this cluster. - * - * @return Kafka admin client properties - */ - Properties getAdminProperties(); + /** + * Get additional Kafka admin client properties for this cluster. + * + * @return Kafka admin client properties + */ + Properties getAdminProperties(); - /** - * Get the set of cluster identifiers that this config can provide configuration for. - * For single-cluster setups, this returns a singleton set. - * - * @return set of cluster names - */ - Set getAvailableClusters(); + /** + * Get the set of cluster identifiers that this config can provide configuration for. For + * single-cluster setups, this returns a singleton set. + * + * @return set of cluster names + */ + Set getAvailableClusters(); - /** - * Get cluster configuration for a specific cluster. - * For multi-cluster setups, returns configuration for the named cluster. - * - * @param clusterName the cluster to get configuration for - * @return ClusterConfig for the specified cluster, or null if not found - */ - default ClusterConfig forCluster(String clusterName) { - if (getClusterName().equals(clusterName)) { - return this; - } - return null; + /** + * Get cluster configuration for a specific cluster. For multi-cluster setups, returns + * configuration for the named cluster. + * + * @param clusterName the cluster to get configuration for + * @return ClusterConfig for the specified cluster, or null if not found + */ + default ClusterConfig forCluster(String clusterName) { + if (getClusterName().equals(clusterName)) { + return this; } + return null; + } - /** - * Get optional security properties (e.g., SASL, SSL). - * - * @return security properties map - */ - default Map getSecurityProperties() { - return Map.of(); - } + /** + * Get optional security properties (e.g., SASL, SSL). + * + * @return security properties map + */ + default Map getSecurityProperties() { + return Map.of(); + } } diff --git a/src/main/java/com/uber/ugroup/config/EnvironmentClusterConfig.java b/src/main/java/com/uber/ugroup/config/EnvironmentClusterConfig.java index 14eaaa0..a552108 100644 --- a/src/main/java/com/uber/ugroup/config/EnvironmentClusterConfig.java +++ b/src/main/java/com/uber/ugroup/config/EnvironmentClusterConfig.java @@ -20,72 +20,72 @@ import java.util.Set; /** - * Simple ClusterConfig implementation backed by environment variables. - * Useful for containerized deployments. + * Simple ClusterConfig implementation backed by environment variables. Useful for containerized + * deployments. */ public class EnvironmentClusterConfig implements ClusterConfig { - private final String clusterName; - private final String bootstrapServers; - private final Properties consumerProperties; - private final Properties adminProperties; + private final String clusterName; + private final String bootstrapServers; + private final Properties consumerProperties; + private final Properties adminProperties; - public EnvironmentClusterConfig() { - this.clusterName = getEnvOrDefault("UGROUP_CLUSTER_NAME", "default"); - this.bootstrapServers = getEnvOrDefault("KAFKA_BOOTSTRAP_SERVERS", "localhost:9092"); - this.consumerProperties = loadPropertiesFromEnv("KAFKA_CONSUMER_"); - this.adminProperties = loadPropertiesFromEnv("KAFKA_ADMIN_"); - } + public EnvironmentClusterConfig() { + this.clusterName = getEnvOrDefault("UGROUP_CLUSTER_NAME", "default"); + this.bootstrapServers = getEnvOrDefault("KAFKA_BOOTSTRAP_SERVERS", "localhost:9092"); + this.consumerProperties = loadPropertiesFromEnv("KAFKA_CONSUMER_"); + this.adminProperties = loadPropertiesFromEnv("KAFKA_ADMIN_"); + } - private String getEnvOrDefault(String key, String defaultValue) { - String value = System.getenv(key); - return value != null ? value : defaultValue; - } + private String getEnvOrDefault(String key, String defaultValue) { + String value = System.getenv(key); + return value != null ? value : defaultValue; + } - private Properties loadPropertiesFromEnv(String prefix) { - Properties props = new Properties(); - System.getenv().forEach((key, value) -> { - if (key.startsWith(prefix)) { - String propKey = key.substring(prefix.length()) - .toLowerCase() - .replace('_', '.'); + private Properties loadPropertiesFromEnv(String prefix) { + Properties props = new Properties(); + System.getenv() + .forEach( + (key, value) -> { + if (key.startsWith(prefix)) { + String propKey = key.substring(prefix.length()).toLowerCase().replace('_', '.'); props.setProperty(propKey, value); - } - }); - return props; - } + } + }); + return props; + } - @Override - public String getClusterName() { - return clusterName; - } + @Override + public String getClusterName() { + return clusterName; + } - @Override - public String getBootstrapServers() { - return bootstrapServers; - } + @Override + public String getBootstrapServers() { + return bootstrapServers; + } - @Override - public Properties getConsumerProperties() { - return consumerProperties; - } + @Override + public Properties getConsumerProperties() { + return consumerProperties; + } - @Override - public Properties getAdminProperties() { - return adminProperties; - } + @Override + public Properties getAdminProperties() { + return adminProperties; + } - @Override - public Set getAvailableClusters() { - return Set.of(clusterName); - } + @Override + public Set getAvailableClusters() { + return Set.of(clusterName); + } - @Override - public Map getSecurityProperties() { - String protocol = System.getenv("KAFKA_SECURITY_PROTOCOL"); - if (protocol != null) { - return Map.of("security.protocol", protocol); - } - return Map.of(); + @Override + public Map getSecurityProperties() { + String protocol = System.getenv("KAFKA_SECURITY_PROTOCOL"); + if (protocol != null) { + return Map.of("security.protocol", protocol); } + return Map.of(); + } } diff --git a/src/main/java/com/uber/ugroup/config/UGroupAutoConfiguration.java b/src/main/java/com/uber/ugroup/config/UGroupAutoConfiguration.java index 51963c0..603ea58 100644 --- a/src/main/java/com/uber/ugroup/config/UGroupAutoConfiguration.java +++ b/src/main/java/com/uber/ugroup/config/UGroupAutoConfiguration.java @@ -33,6 +33,13 @@ import com.uber.ugroup.watchlist.StaticWatchListProvider; import com.uber.ugroup.watchlist.WatchListProvider; import io.micrometer.core.instrument.MeterRegistry; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.common.internals.Topic; @@ -45,237 +52,233 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - /** - * Auto-configuration for uGroup. - * Uses @ConditionalOnMissingBean to allow custom implementations to be provided. + * Auto-configuration for uGroup. Uses @ConditionalOnMissingBean to allow custom implementations to + * be provided. */ @Configuration @EnableConfigurationProperties(UGroupProperties.class) public class UGroupAutoConfiguration { - private static final Logger logger = LoggerFactory.getLogger(UGroupAutoConfiguration.class); - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = "ugroup.metrics.enabled", havingValue = "true", matchIfMissing = true) - public MetricsProvider metricsProvider(MeterRegistry registry) { - logger.info("Creating MicrometerMetricsProvider"); - return new MicrometerMetricsProvider(registry); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = "ugroup.metrics.enabled", havingValue = "false") - public MetricsProvider noopMetricsProvider() { - logger.info("Metrics disabled, using NoopMetricsProvider"); - return NoopMetricsProvider.INSTANCE; + private static final Logger logger = LoggerFactory.getLogger(UGroupAutoConfiguration.class); + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty( + name = "ugroup.metrics.enabled", + havingValue = "true", + matchIfMissing = true) + public MetricsProvider metricsProvider(MeterRegistry registry) { + logger.info("Creating MicrometerMetricsProvider"); + return new MicrometerMetricsProvider(registry); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "ugroup.metrics.enabled", havingValue = "false") + public MetricsProvider noopMetricsProvider() { + logger.info("Metrics disabled, using NoopMetricsProvider"); + return NoopMetricsProvider.INSTANCE; + } + + @Bean + @ConditionalOnMissingBean + public ClusterConfig clusterConfig(UGroupProperties properties) { + logger.info("Creating ClusterConfig for cluster: {}", properties.getKafka().getClusterName()); + return YamlClusterConfig.single( + properties.getKafka().getClusterName(), properties.getKafka().getBootstrapServers()); + } + + @Bean + @ConditionalOnMissingBean + public OffsetFetcher offsetFetcher(ClusterConfig clusterConfig) { + logger.info("Creating DirectOffsetFetcher"); + return new DirectOffsetFetcher(clusterConfig); + } + + @Bean + @ConditionalOnMissingBean + public OffsetCache offsetCache(UGroupProperties properties) { + String cacheType = properties.getCache().getType(); + logger.info("Creating OffsetCache of type: {}", cacheType); + + return switch (cacheType.toLowerCase()) { + case "caffeine" -> + new CaffeineOffsetCache( + properties.getCache().getMaxSize(), + Duration.ofSeconds(properties.getCache().getTtlSeconds())); + case "memory" -> new InMemoryOffsetCache(); + case "none" -> NoopOffsetCache.INSTANCE; + default -> { + logger.warn("Unknown cache type '{}', using caffeine", cacheType); + yield new CaffeineOffsetCache(); + } + }; + } + + @Bean + @ConditionalOnMissingBean + public BlockList blockList(UGroupProperties properties) { + String blocklistFile = properties.getWatchlist().getBlocklistFile(); + if (blocklistFile != null && !blocklistFile.isEmpty()) { + logger.info("Loading blocklist from: {}", blocklistFile); + return BlockList.fromYaml(blocklistFile); } - - @Bean - @ConditionalOnMissingBean - public ClusterConfig clusterConfig(UGroupProperties properties) { - logger.info("Creating ClusterConfig for cluster: {}", properties.getKafka().getClusterName()); - return YamlClusterConfig.single( - properties.getKafka().getClusterName(), - properties.getKafka().getBootstrapServers()); - } - - @Bean - @ConditionalOnMissingBean - public OffsetFetcher offsetFetcher(ClusterConfig clusterConfig) { - logger.info("Creating DirectOffsetFetcher"); - return new DirectOffsetFetcher(clusterConfig); - } - - @Bean - @ConditionalOnMissingBean - public OffsetCache offsetCache(UGroupProperties properties) { - String cacheType = properties.getCache().getType(); - logger.info("Creating OffsetCache of type: {}", cacheType); - - return switch (cacheType.toLowerCase()) { - case "caffeine" -> new CaffeineOffsetCache( - properties.getCache().getMaxSize(), - Duration.ofSeconds(properties.getCache().getTtlSeconds())); - case "memory" -> new InMemoryOffsetCache(); - case "none" -> NoopOffsetCache.INSTANCE; - default -> { - logger.warn("Unknown cache type '{}', using caffeine", cacheType); - yield new CaffeineOffsetCache(); - } - }; - } - - @Bean - @ConditionalOnMissingBean - public BlockList blockList(UGroupProperties properties) { - String blocklistFile = properties.getWatchlist().getBlocklistFile(); - if (blocklistFile != null && !blocklistFile.isEmpty()) { - logger.info("Loading blocklist from: {}", blocklistFile); - return BlockList.fromYaml(blocklistFile); - } - return BlockList.empty(); - } - - @Bean - @ConditionalOnMissingBean - public List watchListProviders(UGroupProperties properties) { - String mode = properties.getWatchlist().getMode(); - logger.info("Creating WatchListProviders with mode: {}", mode); - - List providers = new ArrayList<>(); - - switch (mode.toLowerCase()) { - case "static" -> { - String staticFile = properties.getWatchlist().getStaticFile(); - if (staticFile != null && !staticFile.isEmpty()) { - providers.add(new StaticWatchListProvider( - staticFile, - properties.getProcessing().getConsumerOffsetsPartitionCount())); - } - } - case "regex" -> { - providers.add(new RegexWatchListProvider( - properties.getWatchlist().getIncludePatterns(), - properties.getWatchlist().getExcludePatterns())); - } + return BlockList.empty(); + } + + @Bean + @ConditionalOnMissingBean + public List watchListProviders(UGroupProperties properties) { + String mode = properties.getWatchlist().getMode(); + logger.info("Creating WatchListProviders with mode: {}", mode); + + List providers = new ArrayList<>(); + + switch (mode.toLowerCase()) { + case "static" -> { + String staticFile = properties.getWatchlist().getStaticFile(); + if (staticFile != null && !staticFile.isEmpty()) { + providers.add( + new StaticWatchListProvider( + staticFile, properties.getProcessing().getConsumerOffsetsPartitionCount())); } - - // Always add all-consumers as fallback - providers.add(new AllConsumersWatchListProvider()); - - return providers; - } - - @Bean(destroyMethod = "") - @ConditionalOnMissingBean - public KafkaConsumer consumerOffsetsConsumer(UGroupProperties properties) { - Properties props = new Properties(); - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, properties.getKafka().getBootstrapServers()); - props.put(ConsumerConfig.GROUP_ID_CONFIG, properties.getKafka().getConsumerGroup()); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName()); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName()); - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); - props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "500"); - - if (!"PLAINTEXT".equalsIgnoreCase(properties.getKafka().getSecurityProtocol())) { - props.put("security.protocol", properties.getKafka().getSecurityProtocol()); - } - - logger.info("Creating Kafka consumer for __consumer_offsets with group: {}", - properties.getKafka().getConsumerGroup()); - - KafkaConsumer consumer = new KafkaConsumer<>(props); - consumer.subscribe(Collections.singletonList(Topic.GROUP_METADATA_TOPIC_NAME)); - - return consumer; + } + case "regex" -> { + providers.add( + new RegexWatchListProvider( + properties.getWatchlist().getIncludePatterns(), + properties.getWatchlist().getExcludePatterns())); + } } - @Bean - @ConditionalOnMissingBean - public List compactedOffsetsProcessors( - MetricsProvider metricsProvider, - OffsetFetcher offsetFetcher, - List watchListProviders, - UGroupProperties properties) { - - List processors = new ArrayList<>(); - for (WatchListProvider provider : watchListProviders) { - processors.add(new BaseCompactedOffsetsProcessor( - metricsProvider, - offsetFetcher, - provider, - properties.getKafka().getClusterName())); - } - - logger.info("Created {} compacted offsets processors", processors.size()); - return processors; + // Always add all-consumers as fallback + providers.add(new AllConsumersWatchListProvider()); + + return providers; + } + + @Bean(destroyMethod = "") + @ConditionalOnMissingBean + public KafkaConsumer consumerOffsetsConsumer(UGroupProperties properties) { + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, properties.getKafka().getBootstrapServers()); + props.put(ConsumerConfig.GROUP_ID_CONFIG, properties.getKafka().getConsumerGroup()); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName()); + props.put( + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName()); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); + props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "500"); + + if (!"PLAINTEXT".equalsIgnoreCase(properties.getKafka().getSecurityProtocol())) { + props.put("security.protocol", properties.getKafka().getSecurityProtocol()); } - @Bean - public ConsumerOffsetTopicProcessor consumerOffsetTopicProcessor( - MetricsProvider metricsProvider, - UGroupProperties properties, - KafkaConsumer consumer, - List compactedOffsetsProcessors, - OffsetFetcher offsetFetcher, - BlockList blockList, - List watchListProviders) { - - logger.info("Creating ConsumerOffsetTopicProcessor"); - return new ConsumerOffsetTopicProcessor( - metricsProvider, - properties, - consumer, - compactedOffsetsProcessors, - offsetFetcher, - blockList, - watchListProviders); + logger.info( + "Creating Kafka consumer for __consumer_offsets with group: {}", + properties.getKafka().getConsumerGroup()); + + KafkaConsumer consumer = new KafkaConsumer<>(props); + consumer.subscribe(Collections.singletonList(Topic.GROUP_METADATA_TOPIC_NAME)); + + return consumer; + } + + @Bean + @ConditionalOnMissingBean + public List compactedOffsetsProcessors( + MetricsProvider metricsProvider, + OffsetFetcher offsetFetcher, + List watchListProviders, + UGroupProperties properties) { + + List processors = new ArrayList<>(); + for (WatchListProvider provider : watchListProviders) { + processors.add( + new BaseCompactedOffsetsProcessor( + metricsProvider, offsetFetcher, provider, properties.getKafka().getClusterName())); } - @Bean - public ExecutorService processorExecutorService( - ConsumerOffsetTopicProcessor processor, - UGroupProperties properties) { - - int threadCount = properties.getProcessing().getThreadCount(); - ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - - logger.info("Starting {} processor threads", threadCount); - for (int i = 0; i < threadCount; i++) { - executorService.submit(processor); - } - - return executorService; + logger.info("Created {} compacted offsets processors", processors.size()); + return processors; + } + + @Bean + public ConsumerOffsetTopicProcessor consumerOffsetTopicProcessor( + MetricsProvider metricsProvider, + UGroupProperties properties, + KafkaConsumer consumer, + List compactedOffsetsProcessors, + OffsetFetcher offsetFetcher, + BlockList blockList, + List watchListProviders) { + + logger.info("Creating ConsumerOffsetTopicProcessor"); + return new ConsumerOffsetTopicProcessor( + metricsProvider, + properties, + consumer, + compactedOffsetsProcessors, + offsetFetcher, + blockList, + watchListProviders); + } + + @Bean + public ExecutorService processorExecutorService( + ConsumerOffsetTopicProcessor processor, UGroupProperties properties) { + + int threadCount = properties.getProcessing().getThreadCount(); + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + + logger.info("Starting {} processor threads", threadCount); + for (int i = 0; i < threadCount; i++) { + executorService.submit(processor); } - /** - * Lifecycle bean that ensures the processor stops and the executor shuts down - * before Spring destroys the KafkaConsumer bean. - */ - @Bean - public ProcessorLifecycle processorLifecycle( - ConsumerOffsetTopicProcessor processor, - ExecutorService processorExecutorService, - KafkaConsumer consumerOffsetsConsumer) { - return new ProcessorLifecycle(processor, processorExecutorService, consumerOffsetsConsumer); + return executorService; + } + + /** + * Lifecycle bean that ensures the processor stops and the executor shuts down before Spring + * destroys the KafkaConsumer bean. + */ + @Bean + public ProcessorLifecycle processorLifecycle( + ConsumerOffsetTopicProcessor processor, + ExecutorService processorExecutorService, + KafkaConsumer consumerOffsetsConsumer) { + return new ProcessorLifecycle(processor, processorExecutorService, consumerOffsetsConsumer); + } + + static class ProcessorLifecycle implements org.springframework.beans.factory.DisposableBean { + private final ConsumerOffsetTopicProcessor processor; + private final ExecutorService executorService; + private final KafkaConsumer consumer; + + ProcessorLifecycle( + ConsumerOffsetTopicProcessor processor, + ExecutorService executorService, + KafkaConsumer consumer) { + this.processor = processor; + this.executorService = executorService; + this.consumer = consumer; } - static class ProcessorLifecycle implements org.springframework.beans.factory.DisposableBean { - private final ConsumerOffsetTopicProcessor processor; - private final ExecutorService executorService; - private final KafkaConsumer consumer; - - ProcessorLifecycle(ConsumerOffsetTopicProcessor processor, - ExecutorService executorService, - KafkaConsumer consumer) { - this.processor = processor; - this.executorService = executorService; - this.consumer = consumer; - } - - @Override - public void destroy() { - Logger log = LoggerFactory.getLogger(ProcessorLifecycle.class); - log.info("Shutting down processor lifecycle"); - processor.close(); - executorService.shutdownNow(); - try { - executorService.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - consumer.close(java.time.Duration.ofSeconds(5)); - log.info("Processor lifecycle shutdown complete"); - } + @Override + public void destroy() { + Logger log = LoggerFactory.getLogger(ProcessorLifecycle.class); + log.info("Shutting down processor lifecycle"); + processor.close(); + executorService.shutdownNow(); + try { + executorService.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + consumer.close(java.time.Duration.ofSeconds(5)); + log.info("Processor lifecycle shutdown complete"); } + } } diff --git a/src/main/java/com/uber/ugroup/config/UGroupProperties.java b/src/main/java/com/uber/ugroup/config/UGroupProperties.java index 56c94a3..af7c078 100644 --- a/src/main/java/com/uber/ugroup/config/UGroupProperties.java +++ b/src/main/java/com/uber/ugroup/config/UGroupProperties.java @@ -15,226 +15,223 @@ */ package com.uber.ugroup.config; -import org.springframework.boot.context.properties.ConfigurationProperties; - import java.util.ArrayList; import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; -/** - * Configuration properties for uGroup. - */ +/** Configuration properties for uGroup. */ @ConfigurationProperties(prefix = "ugroup") public class UGroupProperties { - private final Kafka kafka = new Kafka(); - private final Processing processing = new Processing(); - private final Watchlist watchlist = new Watchlist(); - private final Metrics metrics = new Metrics(); - private final Cache cache = new Cache(); + private final Kafka kafka = new Kafka(); + private final Processing processing = new Processing(); + private final Watchlist watchlist = new Watchlist(); + private final Metrics metrics = new Metrics(); + private final Cache cache = new Cache(); - public Kafka getKafka() { - return kafka; - } + public Kafka getKafka() { + return kafka; + } - public Processing getProcessing() { - return processing; - } + public Processing getProcessing() { + return processing; + } - public Watchlist getWatchlist() { - return watchlist; - } - - public Metrics getMetrics() { - return metrics; - } - - public Cache getCache() { - return cache; - } + public Watchlist getWatchlist() { + return watchlist; + } - public static class Kafka { - private String bootstrapServers = "localhost:9092"; - private String consumerGroup = "ugroup-monitor"; - private String clusterName = "default"; - private String securityProtocol = "PLAINTEXT"; + public Metrics getMetrics() { + return metrics; + } - public String getBootstrapServers() { - return bootstrapServers; - } + public Cache getCache() { + return cache; + } - public void setBootstrapServers(String bootstrapServers) { - this.bootstrapServers = bootstrapServers; - } + public static class Kafka { + private String bootstrapServers = "localhost:9092"; + private String consumerGroup = "ugroup-monitor"; + private String clusterName = "default"; + private String securityProtocol = "PLAINTEXT"; - public String getConsumerGroup() { - return consumerGroup; - } + public String getBootstrapServers() { + return bootstrapServers; + } - public void setConsumerGroup(String consumerGroup) { - this.consumerGroup = consumerGroup; - } + public void setBootstrapServers(String bootstrapServers) { + this.bootstrapServers = bootstrapServers; + } - public String getClusterName() { - return clusterName; - } + public String getConsumerGroup() { + return consumerGroup; + } - public void setClusterName(String clusterName) { - this.clusterName = clusterName; - } + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } - public String getSecurityProtocol() { - return securityProtocol; - } + public String getClusterName() { + return clusterName; + } - public void setSecurityProtocol(String securityProtocol) { - this.securityProtocol = securityProtocol; - } + public void setClusterName(String clusterName) { + this.clusterName = clusterName; } - public static class Processing { - private int threadCount = 1; - private int parallelism = 8; - private long compactionIntervalMs = 5000; - private long lagReportIntervalMs = 10000; - private int consumerOffsetsPartitionCount = 50; - private long stuckPartitionThresholdMs = 300000; + public String getSecurityProtocol() { + return securityProtocol; + } - public int getThreadCount() { - return threadCount; - } + public void setSecurityProtocol(String securityProtocol) { + this.securityProtocol = securityProtocol; + } + } + + public static class Processing { + private int threadCount = 1; + private int parallelism = 8; + private long compactionIntervalMs = 5000; + private long lagReportIntervalMs = 10000; + private int consumerOffsetsPartitionCount = 50; + private long stuckPartitionThresholdMs = 300000; + + public int getThreadCount() { + return threadCount; + } - public void setThreadCount(int threadCount) { - this.threadCount = threadCount; - } + public void setThreadCount(int threadCount) { + this.threadCount = threadCount; + } - public int getParallelism() { - return parallelism; - } + public int getParallelism() { + return parallelism; + } - public void setParallelism(int parallelism) { - this.parallelism = parallelism; - } + public void setParallelism(int parallelism) { + this.parallelism = parallelism; + } - public long getCompactionIntervalMs() { - return compactionIntervalMs; - } + public long getCompactionIntervalMs() { + return compactionIntervalMs; + } - public void setCompactionIntervalMs(long compactionIntervalMs) { - this.compactionIntervalMs = compactionIntervalMs; - } + public void setCompactionIntervalMs(long compactionIntervalMs) { + this.compactionIntervalMs = compactionIntervalMs; + } - public long getLagReportIntervalMs() { - return lagReportIntervalMs; - } + public long getLagReportIntervalMs() { + return lagReportIntervalMs; + } - public void setLagReportIntervalMs(long lagReportIntervalMs) { - this.lagReportIntervalMs = lagReportIntervalMs; - } + public void setLagReportIntervalMs(long lagReportIntervalMs) { + this.lagReportIntervalMs = lagReportIntervalMs; + } - public int getConsumerOffsetsPartitionCount() { - return consumerOffsetsPartitionCount; - } + public int getConsumerOffsetsPartitionCount() { + return consumerOffsetsPartitionCount; + } - public void setConsumerOffsetsPartitionCount(int consumerOffsetsPartitionCount) { - this.consumerOffsetsPartitionCount = consumerOffsetsPartitionCount; - } + public void setConsumerOffsetsPartitionCount(int consumerOffsetsPartitionCount) { + this.consumerOffsetsPartitionCount = consumerOffsetsPartitionCount; + } - public long getStuckPartitionThresholdMs() { - return stuckPartitionThresholdMs; - } + public long getStuckPartitionThresholdMs() { + return stuckPartitionThresholdMs; + } - public void setStuckPartitionThresholdMs(long stuckPartitionThresholdMs) { - this.stuckPartitionThresholdMs = stuckPartitionThresholdMs; - } + public void setStuckPartitionThresholdMs(long stuckPartitionThresholdMs) { + this.stuckPartitionThresholdMs = stuckPartitionThresholdMs; } + } - public static class Watchlist { - private String mode = "all"; // all, static, regex - private String staticFile; - private String blocklistFile; - private List includePatterns = new ArrayList<>(); - private List excludePatterns = new ArrayList<>(); + public static class Watchlist { + private String mode = "all"; // all, static, regex + private String staticFile; + private String blocklistFile; + private List includePatterns = new ArrayList<>(); + private List excludePatterns = new ArrayList<>(); - public String getMode() { - return mode; - } + public String getMode() { + return mode; + } - public void setMode(String mode) { - this.mode = mode; - } + public void setMode(String mode) { + this.mode = mode; + } - public String getStaticFile() { - return staticFile; - } + public String getStaticFile() { + return staticFile; + } - public void setStaticFile(String staticFile) { - this.staticFile = staticFile; - } + public void setStaticFile(String staticFile) { + this.staticFile = staticFile; + } - public String getBlocklistFile() { - return blocklistFile; - } + public String getBlocklistFile() { + return blocklistFile; + } - public void setBlocklistFile(String blocklistFile) { - this.blocklistFile = blocklistFile; - } + public void setBlocklistFile(String blocklistFile) { + this.blocklistFile = blocklistFile; + } - public List getIncludePatterns() { - return includePatterns; - } + public List getIncludePatterns() { + return includePatterns; + } - public void setIncludePatterns(List includePatterns) { - this.includePatterns = includePatterns; - } + public void setIncludePatterns(List includePatterns) { + this.includePatterns = includePatterns; + } - public List getExcludePatterns() { - return excludePatterns; - } + public List getExcludePatterns() { + return excludePatterns; + } - public void setExcludePatterns(List excludePatterns) { - this.excludePatterns = excludePatterns; - } + public void setExcludePatterns(List excludePatterns) { + this.excludePatterns = excludePatterns; } + } - public static class Metrics { - private boolean enabled = true; + public static class Metrics { + private boolean enabled = true; - public boolean isEnabled() { - return enabled; - } + public boolean isEnabled() { + return enabled; + } - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } + public void setEnabled(boolean enabled) { + this.enabled = enabled; } + } - public static class Cache { - private String type = "caffeine"; // caffeine, memory, none - private int maxSize = 10000; - private int ttlSeconds = 300; + public static class Cache { + private String type = "caffeine"; // caffeine, memory, none + private int maxSize = 10000; + private int ttlSeconds = 300; - public String getType() { - return type; - } + public String getType() { + return type; + } - public void setType(String type) { - this.type = type; - } + public void setType(String type) { + this.type = type; + } - public int getMaxSize() { - return maxSize; - } + public int getMaxSize() { + return maxSize; + } - public void setMaxSize(int maxSize) { - this.maxSize = maxSize; - } + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } - public int getTtlSeconds() { - return ttlSeconds; - } + public int getTtlSeconds() { + return ttlSeconds; + } - public void setTtlSeconds(int ttlSeconds) { - this.ttlSeconds = ttlSeconds; - } + public void setTtlSeconds(int ttlSeconds) { + this.ttlSeconds = ttlSeconds; } + } } diff --git a/src/main/java/com/uber/ugroup/config/YamlClusterConfig.java b/src/main/java/com/uber/ugroup/config/YamlClusterConfig.java index 7f44064..c2c5bda 100644 --- a/src/main/java/com/uber/ugroup/config/YamlClusterConfig.java +++ b/src/main/java/com/uber/ugroup/config/YamlClusterConfig.java @@ -15,12 +15,6 @@ */ package com.uber.ugroup.config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; - import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -29,11 +23,17 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; /** * ClusterConfig implementation that reads cluster configuration from a YAML file. * - * Expected YAML format: + *

Expected YAML format: + * *

  * clusters:
  *   my-cluster:
@@ -44,110 +44,113 @@
  */
 public class YamlClusterConfig implements ClusterConfig {
 
-    private static final Logger logger = LoggerFactory.getLogger(YamlClusterConfig.class);
-
-    private final String clusterName;
-    private final String bootstrapServers;
-    private final Properties consumerProperties;
-    private final Properties adminProperties;
-    private final Map allClusters;
+  private static final Logger logger = LoggerFactory.getLogger(YamlClusterConfig.class);
 
-    public YamlClusterConfig(String yamlFilePath, String clusterName) {
-        this.allClusters = loadFromFile(yamlFilePath);
-        YamlClusterConfig config = allClusters.get(clusterName);
-        if (config == null) {
-            throw new IllegalArgumentException("Cluster '" + clusterName + "' not found in " + yamlFilePath);
-        }
-        this.clusterName = config.clusterName;
-        this.bootstrapServers = config.bootstrapServers;
-        this.consumerProperties = config.consumerProperties;
-        this.adminProperties = config.adminProperties;
-    }
+  private final String clusterName;
+  private final String bootstrapServers;
+  private final Properties consumerProperties;
+  private final Properties adminProperties;
+  private final Map allClusters;
 
-    private YamlClusterConfig(String clusterName, String bootstrapServers,
-                              Properties consumerProperties, Properties adminProperties) {
-        this.clusterName = clusterName;
-        this.bootstrapServers = bootstrapServers;
-        this.consumerProperties = consumerProperties;
-        this.adminProperties = adminProperties;
-        this.allClusters = Collections.emptyMap();
+  public YamlClusterConfig(String yamlFilePath, String clusterName) {
+    this.allClusters = loadFromFile(yamlFilePath);
+    YamlClusterConfig config = allClusters.get(clusterName);
+    if (config == null) {
+      throw new IllegalArgumentException(
+          "Cluster '" + clusterName + "' not found in " + yamlFilePath);
     }
-
-    @SuppressWarnings("unchecked")
-    private Map loadFromFile(String yamlFilePath) {
-        Map result = new HashMap<>();
-        Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
-
-        try (InputStream input = new FileInputStream(yamlFilePath)) {
-            Map root = yaml.load(input);
-            Map clusters = (Map) root.get("clusters");
-
-            if (clusters != null) {
-                for (Map.Entry entry : clusters.entrySet()) {
-                    String name = entry.getKey();
-                    Map clusterData = (Map) entry.getValue();
-
-                    String bootstrap = (String) clusterData.get("bootstrap-servers");
-                    Map propsMap = (Map) clusterData.getOrDefault("properties", Map.of());
-
-                    Properties props = new Properties();
-                    propsMap.forEach((k, v) -> props.setProperty(k, String.valueOf(v)));
-
-                    result.put(name, new YamlClusterConfig(name, bootstrap, props, new Properties(props)));
-                }
-            }
-        } catch (IOException e) {
-            logger.error("Failed to load cluster config from {}", yamlFilePath, e);
-            throw new RuntimeException("Failed to load cluster config", e);
-        }
-
-        return result;
-    }
-
-    @Override
-    public String getClusterName() {
-        return clusterName;
-    }
-
-    @Override
-    public String getBootstrapServers() {
-        return bootstrapServers;
-    }
-
-    @Override
-    public Properties getConsumerProperties() {
-        return consumerProperties;
-    }
-
-    @Override
-    public Properties getAdminProperties() {
-        return adminProperties;
-    }
-
-    @Override
-    public Set getAvailableClusters() {
-        return allClusters.isEmpty() ? Set.of(clusterName) : allClusters.keySet();
-    }
-
-    @Override
-    public ClusterConfig forCluster(String clusterName) {
-        if (this.clusterName.equals(clusterName)) {
-            return this;
+    this.clusterName = config.clusterName;
+    this.bootstrapServers = config.bootstrapServers;
+    this.consumerProperties = config.consumerProperties;
+    this.adminProperties = config.adminProperties;
+  }
+
+  private YamlClusterConfig(
+      String clusterName,
+      String bootstrapServers,
+      Properties consumerProperties,
+      Properties adminProperties) {
+    this.clusterName = clusterName;
+    this.bootstrapServers = bootstrapServers;
+    this.consumerProperties = consumerProperties;
+    this.adminProperties = adminProperties;
+    this.allClusters = Collections.emptyMap();
+  }
+
+  @SuppressWarnings("unchecked")
+  private Map loadFromFile(String yamlFilePath) {
+    Map result = new HashMap<>();
+    Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
+
+    try (InputStream input = new FileInputStream(yamlFilePath)) {
+      Map root = yaml.load(input);
+      Map clusters = (Map) root.get("clusters");
+
+      if (clusters != null) {
+        for (Map.Entry entry : clusters.entrySet()) {
+          String name = entry.getKey();
+          Map clusterData = (Map) entry.getValue();
+
+          String bootstrap = (String) clusterData.get("bootstrap-servers");
+          Map propsMap =
+              (Map) clusterData.getOrDefault("properties", Map.of());
+
+          Properties props = new Properties();
+          propsMap.forEach((k, v) -> props.setProperty(k, String.valueOf(v)));
+
+          result.put(name, new YamlClusterConfig(name, bootstrap, props, new Properties(props)));
         }
-        return allClusters.get(clusterName);
-    }
-
-    /**
-     * Create a simple single-cluster config programmatically.
-     */
-    public static YamlClusterConfig single(String clusterName, String bootstrapServers) {
-        return new YamlClusterConfig(clusterName, bootstrapServers, new Properties(), new Properties());
+      }
+    } catch (IOException e) {
+      logger.error("Failed to load cluster config from {}", yamlFilePath, e);
+      throw new RuntimeException("Failed to load cluster config", e);
     }
 
-    /**
-     * Create a simple single-cluster config with additional properties.
-     */
-    public static YamlClusterConfig single(String clusterName, String bootstrapServers, Properties properties) {
-        return new YamlClusterConfig(clusterName, bootstrapServers, properties, new Properties(properties));
+    return result;
+  }
+
+  @Override
+  public String getClusterName() {
+    return clusterName;
+  }
+
+  @Override
+  public String getBootstrapServers() {
+    return bootstrapServers;
+  }
+
+  @Override
+  public Properties getConsumerProperties() {
+    return consumerProperties;
+  }
+
+  @Override
+  public Properties getAdminProperties() {
+    return adminProperties;
+  }
+
+  @Override
+  public Set getAvailableClusters() {
+    return allClusters.isEmpty() ? Set.of(clusterName) : allClusters.keySet();
+  }
+
+  @Override
+  public ClusterConfig forCluster(String clusterName) {
+    if (this.clusterName.equals(clusterName)) {
+      return this;
     }
+    return allClusters.get(clusterName);
+  }
+
+  /** Create a simple single-cluster config programmatically. */
+  public static YamlClusterConfig single(String clusterName, String bootstrapServers) {
+    return new YamlClusterConfig(clusterName, bootstrapServers, new Properties(), new Properties());
+  }
+
+  /** Create a simple single-cluster config with additional properties. */
+  public static YamlClusterConfig single(
+      String clusterName, String bootstrapServers, Properties properties) {
+    return new YamlClusterConfig(
+        clusterName, bootstrapServers, properties, new Properties(properties));
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/fetcher/DirectOffsetFetcher.java b/src/main/java/com/uber/ugroup/fetcher/DirectOffsetFetcher.java
index 37ea73c..c2dcb21 100644
--- a/src/main/java/com/uber/ugroup/fetcher/DirectOffsetFetcher.java
+++ b/src/main/java/com/uber/ugroup/fetcher/DirectOffsetFetcher.java
@@ -16,6 +16,16 @@
 package com.uber.ugroup.fetcher;
 
 import com.uber.ugroup.config.ClusterConfig;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import org.apache.kafka.clients.admin.AdminClient;
 import org.apache.kafka.clients.admin.ConsumerGroupDescription;
 import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsResult;
@@ -29,160 +39,149 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.time.Duration;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * OffsetFetcher implementation that directly queries Kafka using AdminClient and KafkaConsumer.
- */
+/** OffsetFetcher implementation that directly queries Kafka using AdminClient and KafkaConsumer. */
 public class DirectOffsetFetcher implements OffsetFetcher {
 
-    private static final Logger logger = LoggerFactory.getLogger(DirectOffsetFetcher.class);
-    private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
-
-    private final AdminClient adminClient;
-    private final KafkaConsumer consumer;
-    private final Duration timeout;
-
-    public DirectOffsetFetcher(ClusterConfig clusterConfig) {
-        this(clusterConfig, DEFAULT_TIMEOUT);
-    }
-
-    public DirectOffsetFetcher(ClusterConfig clusterConfig, Duration timeout) {
-        this.timeout = timeout;
-        this.adminClient = createAdminClient(clusterConfig);
-        this.consumer = createConsumer(clusterConfig);
+  private static final Logger logger = LoggerFactory.getLogger(DirectOffsetFetcher.class);
+  private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
+
+  private final AdminClient adminClient;
+  private final KafkaConsumer consumer;
+  private final Duration timeout;
+
+  public DirectOffsetFetcher(ClusterConfig clusterConfig) {
+    this(clusterConfig, DEFAULT_TIMEOUT);
+  }
+
+  public DirectOffsetFetcher(ClusterConfig clusterConfig, Duration timeout) {
+    this.timeout = timeout;
+    this.adminClient = createAdminClient(clusterConfig);
+    this.consumer = createConsumer(clusterConfig);
+  }
+
+  private AdminClient createAdminClient(ClusterConfig clusterConfig) {
+    Properties props = new Properties();
+    props.put("bootstrap.servers", clusterConfig.getBootstrapServers());
+    props.put("request.timeout.ms", (int) timeout.toMillis());
+    props.put("default.api.timeout.ms", (int) timeout.toMillis());
+    props.putAll(clusterConfig.getAdminProperties());
+    return AdminClient.create(props);
+  }
+
+  private KafkaConsumer createConsumer(ClusterConfig clusterConfig) {
+    Properties props = new Properties();
+    props.put("bootstrap.servers", clusterConfig.getBootstrapServers());
+    props.put("key.deserializer", ByteArrayDeserializer.class.getName());
+    props.put("value.deserializer", ByteArrayDeserializer.class.getName());
+    props.put("enable.auto.commit", "false");
+    // Don't join any group - this is just for metadata queries
+    props.put("group.id", "ugroup-offset-fetcher-" + System.currentTimeMillis());
+    props.putAll(clusterConfig.getConsumerProperties());
+    return new KafkaConsumer<>(props);
+  }
+
+  @Override
+  public Map beginningOffsets(Collection partitions) {
+    try {
+      return consumer.beginningOffsets(partitions, timeout);
+    } catch (Exception e) {
+      logger.error("Failed to get beginning offsets for {}", partitions, e);
+      return Collections.emptyMap();
     }
-
-    private AdminClient createAdminClient(ClusterConfig clusterConfig) {
-        Properties props = new Properties();
-        props.put("bootstrap.servers", clusterConfig.getBootstrapServers());
-        props.put("request.timeout.ms", (int) timeout.toMillis());
-        props.put("default.api.timeout.ms", (int) timeout.toMillis());
-        props.putAll(clusterConfig.getAdminProperties());
-        return AdminClient.create(props);
+  }
+
+  @Override
+  public Map endOffsets(Collection partitions) {
+    try {
+      return consumer.endOffsets(partitions, timeout);
+    } catch (Exception e) {
+      logger.error("Failed to get end offsets for {}", partitions, e);
+      return Collections.emptyMap();
     }
-
-    private KafkaConsumer createConsumer(ClusterConfig clusterConfig) {
-        Properties props = new Properties();
-        props.put("bootstrap.servers", clusterConfig.getBootstrapServers());
-        props.put("key.deserializer", ByteArrayDeserializer.class.getName());
-        props.put("value.deserializer", ByteArrayDeserializer.class.getName());
-        props.put("enable.auto.commit", "false");
-        // Don't join any group - this is just for metadata queries
-        props.put("group.id", "ugroup-offset-fetcher-" + System.currentTimeMillis());
-        props.putAll(clusterConfig.getConsumerProperties());
-        return new KafkaConsumer<>(props);
+  }
+
+  @Override
+  public List partitionsFor(String topic) {
+    try {
+      return consumer.partitionsFor(topic, timeout);
+    } catch (Exception e) {
+      logger.error("Failed to get partitions for topic {}", topic, e);
+      return null;
     }
-
-    @Override
-    public Map beginningOffsets(Collection partitions) {
-        try {
-            return consumer.beginningOffsets(partitions, timeout);
-        } catch (Exception e) {
-            logger.error("Failed to get beginning offsets for {}", partitions, e);
-            return Collections.emptyMap();
-        }
+  }
+
+  @Override
+  public int partitionCount(String topic) {
+    List partitions = partitionsFor(topic);
+    return partitions != null ? partitions.size() : 0;
+  }
+
+  @Override
+  public Map listConsumerGroupOffsets(String groupId) {
+    try {
+      ListConsumerGroupOffsetsResult result = adminClient.listConsumerGroupOffsets(groupId);
+      return result.partitionsToOffsetAndMetadata().get(timeout.toMillis(), TimeUnit.MILLISECONDS);
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      logger.error("Interrupted while fetching offsets for group {}", groupId, e);
+      return Collections.emptyMap();
+    } catch (ExecutionException | TimeoutException e) {
+      logger.error("Failed to fetch offsets for group {}", groupId, e);
+      return Collections.emptyMap();
     }
-
-    @Override
-    public Map endOffsets(Collection partitions) {
-        try {
-            return consumer.endOffsets(partitions, timeout);
-        } catch (Exception e) {
-            logger.error("Failed to get end offsets for {}", partitions, e);
-            return Collections.emptyMap();
-        }
+  }
+
+  @Override
+  public Map offsetsForTimes(
+      Map timestampsToSearch) {
+    try {
+      return consumer.offsetsForTimes(timestampsToSearch, timeout);
+    } catch (Exception e) {
+      logger.error("Failed to get offsets for times", e);
+      return Collections.emptyMap();
     }
-
-    @Override
-    public List partitionsFor(String topic) {
-        try {
-            return consumer.partitionsFor(topic, timeout);
-        } catch (Exception e) {
-            logger.error("Failed to get partitions for topic {}", topic, e);
-            return null;
+  }
+
+  @Override
+  public Map getConsumerGroupAssignmentForTopic(String groupId, String topic) {
+    try {
+      ConsumerGroupDescription description =
+          adminClient
+              .describeConsumerGroups(List.of(groupId))
+              .describedGroups()
+              .get(groupId)
+              .get(timeout.toMillis(), TimeUnit.MILLISECONDS);
+
+      Map result = new HashMap<>();
+      for (MemberDescription member : description.members()) {
+        for (TopicPartition tp : member.assignment().topicPartitions()) {
+          if (tp.topic().equals(topic)) {
+            result.put(tp.partition(), member.consumerId());
+          }
         }
+      }
+      return result;
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      logger.error("Interrupted while describing group {}", groupId, e);
+      return Collections.emptyMap();
+    } catch (ExecutionException | TimeoutException e) {
+      logger.error("Failed to describe group {}", groupId, e);
+      return Collections.emptyMap();
     }
-
-    @Override
-    public int partitionCount(String topic) {
-        List partitions = partitionsFor(topic);
-        return partitions != null ? partitions.size() : 0;
+  }
+
+  @Override
+  public void close() {
+    try {
+      consumer.close(timeout);
+    } catch (Exception e) {
+      logger.warn("Error closing consumer", e);
     }
-
-    @Override
-    public Map listConsumerGroupOffsets(String groupId) {
-        try {
-            ListConsumerGroupOffsetsResult result = adminClient.listConsumerGroupOffsets(groupId);
-            return result.partitionsToOffsetAndMetadata()
-                    .get(timeout.toMillis(), TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            logger.error("Interrupted while fetching offsets for group {}", groupId, e);
-            return Collections.emptyMap();
-        } catch (ExecutionException | TimeoutException e) {
-            logger.error("Failed to fetch offsets for group {}", groupId, e);
-            return Collections.emptyMap();
-        }
-    }
-
-    @Override
-    public Map offsetsForTimes(Map timestampsToSearch) {
-        try {
-            return consumer.offsetsForTimes(timestampsToSearch, timeout);
-        } catch (Exception e) {
-            logger.error("Failed to get offsets for times", e);
-            return Collections.emptyMap();
-        }
-    }
-
-    @Override
-    public Map getConsumerGroupAssignmentForTopic(String groupId, String topic) {
-        try {
-            ConsumerGroupDescription description = adminClient.describeConsumerGroups(List.of(groupId))
-                    .describedGroups()
-                    .get(groupId)
-                    .get(timeout.toMillis(), TimeUnit.MILLISECONDS);
-
-            Map result = new HashMap<>();
-            for (MemberDescription member : description.members()) {
-                for (TopicPartition tp : member.assignment().topicPartitions()) {
-                    if (tp.topic().equals(topic)) {
-                        result.put(tp.partition(), member.consumerId());
-                    }
-                }
-            }
-            return result;
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            logger.error("Interrupted while describing group {}", groupId, e);
-            return Collections.emptyMap();
-        } catch (ExecutionException | TimeoutException e) {
-            logger.error("Failed to describe group {}", groupId, e);
-            return Collections.emptyMap();
-        }
-    }
-
-    @Override
-    public void close() {
-        try {
-            consumer.close(timeout);
-        } catch (Exception e) {
-            logger.warn("Error closing consumer", e);
-        }
-        try {
-            adminClient.close(timeout);
-        } catch (Exception e) {
-            logger.warn("Error closing admin client", e);
-        }
+    try {
+      adminClient.close(timeout);
+    } catch (Exception e) {
+      logger.warn("Error closing admin client", e);
     }
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/fetcher/OffsetFetcher.java b/src/main/java/com/uber/ugroup/fetcher/OffsetFetcher.java
index 95104ea..92ba169 100644
--- a/src/main/java/com/uber/ugroup/fetcher/OffsetFetcher.java
+++ b/src/main/java/com/uber/ugroup/fetcher/OffsetFetcher.java
@@ -15,80 +15,77 @@
  */
 package com.uber.ugroup.fetcher;
 
-import org.apache.kafka.clients.consumer.OffsetAndMetadata;
-import org.apache.kafka.common.PartitionInfo;
-import org.apache.kafka.common.TopicPartition;
-
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import org.apache.kafka.clients.consumer.OffsetAndMetadata;
+import org.apache.kafka.common.PartitionInfo;
+import org.apache.kafka.common.TopicPartition;
 
 /**
- * Interface for fetching offset and metadata from Kafka.
- * Allows pluggable implementations for different Kafka clients or caching strategies.
+ * Interface for fetching offset and metadata from Kafka. Allows pluggable implementations for
+ * different Kafka clients or caching strategies.
  */
 public interface OffsetFetcher {
 
-    /**
-     * Get the beginning offsets for the given partitions.
-     *
-     * @param partitions the partitions to query
-     * @return map of partition to beginning offset
-     */
-    Map beginningOffsets(Collection partitions);
+  /**
+   * Get the beginning offsets for the given partitions.
+   *
+   * @param partitions the partitions to query
+   * @return map of partition to beginning offset
+   */
+  Map beginningOffsets(Collection partitions);
 
-    /**
-     * Get the end offsets for the given partitions.
-     *
-     * @param partitions the partitions to query
-     * @return map of partition to end offset
-     */
-    Map endOffsets(Collection partitions);
+  /**
+   * Get the end offsets for the given partitions.
+   *
+   * @param partitions the partitions to query
+   * @return map of partition to end offset
+   */
+  Map endOffsets(Collection partitions);
 
-    /**
-     * Get partition information for a topic.
-     *
-     * @param topic the topic name
-     * @return list of partition info, or null if topic doesn't exist
-     */
-    List partitionsFor(String topic);
+  /**
+   * Get partition information for a topic.
+   *
+   * @param topic the topic name
+   * @return list of partition info, or null if topic doesn't exist
+   */
+  List partitionsFor(String topic);
 
-    /**
-     * Get the number of partitions for a topic.
-     *
-     * @param topic the topic name
-     * @return partition count
-     */
-    int partitionCount(String topic);
+  /**
+   * Get the number of partitions for a topic.
+   *
+   * @param topic the topic name
+   * @return partition count
+   */
+  int partitionCount(String topic);
 
-    /**
-     * List committed offsets for a consumer group.
-     *
-     * @param groupId the consumer group ID
-     * @return map of partition to committed offset and metadata
-     */
-    Map listConsumerGroupOffsets(String groupId);
+  /**
+   * List committed offsets for a consumer group.
+   *
+   * @param groupId the consumer group ID
+   * @return map of partition to committed offset and metadata
+   */
+  Map listConsumerGroupOffsets(String groupId);
 
-    /**
-     * Get offsets for specific timestamps.
-     *
-     * @param timestampsToSearch map of partition to timestamp
-     * @return map of partition to offset info
-     */
-    Map offsetsForTimes(
-            Map timestampsToSearch);
+  /**
+   * Get offsets for specific timestamps.
+   *
+   * @param timestampsToSearch map of partition to timestamp
+   * @return map of partition to offset info
+   */
+  Map offsetsForTimes(
+      Map timestampsToSearch);
 
-    /**
-     * Get the partition assignment for a consumer group on a specific topic.
-     *
-     * @param groupId the consumer group ID
-     * @param topic   the topic name
-     * @return map of partition to assigned member ID
-     */
-    Map getConsumerGroupAssignmentForTopic(String groupId, String topic);
+  /**
+   * Get the partition assignment for a consumer group on a specific topic.
+   *
+   * @param groupId the consumer group ID
+   * @param topic the topic name
+   * @return map of partition to assigned member ID
+   */
+  Map getConsumerGroupAssignmentForTopic(String groupId, String topic);
 
-    /**
-     * Close resources used by this fetcher.
-     */
-    void close();
+  /** Close resources used by this fetcher. */
+  void close();
 }
diff --git a/src/main/java/com/uber/ugroup/metrics/MetricsProvider.java b/src/main/java/com/uber/ugroup/metrics/MetricsProvider.java
index e1e5d31..542bdc1 100644
--- a/src/main/java/com/uber/ugroup/metrics/MetricsProvider.java
+++ b/src/main/java/com/uber/ugroup/metrics/MetricsProvider.java
@@ -19,60 +19,60 @@
 import java.util.function.Supplier;
 
 /**
- * Interface for emitting metrics. Allows pluggable metrics implementations
- * (e.g., Micrometer, Datadog, Prometheus).
+ * Interface for emitting metrics. Allows pluggable metrics implementations (e.g., Micrometer,
+ * Datadog, Prometheus).
  */
 public interface MetricsProvider {
 
-    /**
-     * Increment a counter metric.
-     *
-     * @param name the metric name
-     * @param tags additional tags for the metric
-     */
-    void incrementCounter(String name, Map tags);
+  /**
+   * Increment a counter metric.
+   *
+   * @param name the metric name
+   * @param tags additional tags for the metric
+   */
+  void incrementCounter(String name, Map tags);
 
-    /**
-     * Increment a counter metric by a specific amount.
-     *
-     * @param name  the metric name
-     * @param count the amount to increment
-     * @param tags  additional tags for the metric
-     */
-    void incrementCounter(String name, long count, Map tags);
+  /**
+   * Increment a counter metric by a specific amount.
+   *
+   * @param name the metric name
+   * @param count the amount to increment
+   * @param tags additional tags for the metric
+   */
+  void incrementCounter(String name, long count, Map tags);
 
-    /**
-     * Register a gauge metric with a value supplier.
-     *
-     * @param name          the metric name
-     * @param valueSupplier supplier that provides the current value
-     * @param tags          additional tags for the metric
-     */
-    void registerGauge(String name, Supplier valueSupplier, Map tags);
+  /**
+   * Register a gauge metric with a value supplier.
+   *
+   * @param name the metric name
+   * @param valueSupplier supplier that provides the current value
+   * @param tags additional tags for the metric
+   */
+  void registerGauge(String name, Supplier valueSupplier, Map tags);
 
-    /**
-     * Record a gauge value.
-     *
-     * @param name  the metric name
-     * @param value the value to record
-     * @param tags  additional tags for the metric
-     */
-    void recordGauge(String name, double value, Map tags);
+  /**
+   * Record a gauge value.
+   *
+   * @param name the metric name
+   * @param value the value to record
+   * @param tags additional tags for the metric
+   */
+  void recordGauge(String name, double value, Map tags);
 
-    /**
-     * Record a timer/histogram value in milliseconds.
-     *
-     * @param name   the metric name
-     * @param millis the time in milliseconds
-     * @param tags   additional tags for the metric
-     */
-    void recordTimer(String name, long millis, Map tags);
+  /**
+   * Record a timer/histogram value in milliseconds.
+   *
+   * @param name the metric name
+   * @param millis the time in milliseconds
+   * @param tags additional tags for the metric
+   */
+  void recordTimer(String name, long millis, Map tags);
 
-    /**
-     * Create a child metrics provider with additional default tags.
-     *
-     * @param tags tags to add to all metrics from this child
-     * @return a new MetricsProvider with the additional tags
-     */
-    MetricsProvider tagged(Map tags);
+  /**
+   * Create a child metrics provider with additional default tags.
+   *
+   * @param tags tags to add to all metrics from this child
+   * @return a new MetricsProvider with the additional tags
+   */
+  MetricsProvider tagged(Map tags);
 }
diff --git a/src/main/java/com/uber/ugroup/metrics/MicrometerMetricsProvider.java b/src/main/java/com/uber/ugroup/metrics/MicrometerMetricsProvider.java
index 842bfb9..2e30c28 100644
--- a/src/main/java/com/uber/ugroup/metrics/MicrometerMetricsProvider.java
+++ b/src/main/java/com/uber/ugroup/metrics/MicrometerMetricsProvider.java
@@ -20,83 +20,76 @@
 import io.micrometer.core.instrument.MeterRegistry;
 import io.micrometer.core.instrument.Tags;
 import io.micrometer.core.instrument.Timer;
-
 import java.time.Duration;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Supplier;
 
-/**
- * MetricsProvider implementation using Micrometer for Prometheus-compatible metrics.
- */
+/** MetricsProvider implementation using Micrometer for Prometheus-compatible metrics. */
 public class MicrometerMetricsProvider implements MetricsProvider {
 
-    private final MeterRegistry registry;
-    private final Tags baseTags;
-    private final Map registeredGauges = new ConcurrentHashMap<>();
+  private final MeterRegistry registry;
+  private final Tags baseTags;
+  private final Map registeredGauges = new ConcurrentHashMap<>();
 
-    public MicrometerMetricsProvider(MeterRegistry registry) {
-        this(registry, Tags.empty());
-    }
+  public MicrometerMetricsProvider(MeterRegistry registry) {
+    this(registry, Tags.empty());
+  }
 
-    private MicrometerMetricsProvider(MeterRegistry registry, Tags baseTags) {
-        this.registry = registry;
-        this.baseTags = baseTags;
-    }
+  private MicrometerMetricsProvider(MeterRegistry registry, Tags baseTags) {
+    this.registry = registry;
+    this.baseTags = baseTags;
+  }
 
-    @Override
-    public void incrementCounter(String name, Map tags) {
-        incrementCounter(name, 1, tags);
-    }
+  @Override
+  public void incrementCounter(String name, Map tags) {
+    incrementCounter(name, 1, tags);
+  }
 
-    @Override
-    public void incrementCounter(String name, long count, Map tags) {
-        Counter.builder(name)
-                .tags(baseTags)
-                .tags(toTags(tags))
-                .register(registry)
-                .increment(count);
-    }
+  @Override
+  public void incrementCounter(String name, long count, Map tags) {
+    Counter.builder(name).tags(baseTags).tags(toTags(tags)).register(registry).increment(count);
+  }
 
-    @Override
-    public void registerGauge(String name, Supplier valueSupplier, Map tags) {
-        String key = name + tags.toString();
-        registeredGauges.computeIfAbsent(key, k -> {
-            Gauge.builder(name, valueSupplier)
-                    .tags(baseTags)
-                    .tags(toTags(tags))
-                    .register(registry);
-            return valueSupplier;
+  @Override
+  public void registerGauge(String name, Supplier valueSupplier, Map tags) {
+    String key = name + tags.toString();
+    registeredGauges.computeIfAbsent(
+        key,
+        k -> {
+          Gauge.builder(name, valueSupplier).tags(baseTags).tags(toTags(tags)).register(registry);
+          return valueSupplier;
         });
-    }
+  }
 
-    @Override
-    public void recordGauge(String name, double value, Map tags) {
-        // For Micrometer, we use a gauge that tracks the latest value
-        // This uses an AtomicDouble internally via the gauge builder
-        registry.gauge(name, baseTags.and(toTags(tags)), value);
-    }
+  @Override
+  public void recordGauge(String name, double value, Map tags) {
+    // For Micrometer, we use a gauge that tracks the latest value
+    // This uses an AtomicDouble internally via the gauge builder
+    registry.gauge(name, baseTags.and(toTags(tags)), value);
+  }
 
-    @Override
-    public void recordTimer(String name, long millis, Map tags) {
-        Timer.builder(name)
-                .tags(baseTags)
-                .tags(toTags(tags))
-                .register(registry)
-                .record(Duration.ofMillis(millis));
-    }
+  @Override
+  public void recordTimer(String name, long millis, Map tags) {
+    Timer.builder(name)
+        .tags(baseTags)
+        .tags(toTags(tags))
+        .register(registry)
+        .record(Duration.ofMillis(millis));
+  }
 
-    @Override
-    public MetricsProvider tagged(Map tags) {
-        return new MicrometerMetricsProvider(registry, baseTags.and(toTags(tags)));
-    }
+  @Override
+  public MetricsProvider tagged(Map tags) {
+    return new MicrometerMetricsProvider(registry, baseTags.and(toTags(tags)));
+  }
 
-    private Tags toTags(Map tags) {
-        if (tags == null || tags.isEmpty()) {
-            return Tags.empty();
-        }
-        return Tags.of(tags.entrySet().stream()
-                .map(e -> io.micrometer.core.instrument.Tag.of(e.getKey(), e.getValue()))
-                .toList());
+  private Tags toTags(Map tags) {
+    if (tags == null || tags.isEmpty()) {
+      return Tags.empty();
     }
+    return Tags.of(
+        tags.entrySet().stream()
+            .map(e -> io.micrometer.core.instrument.Tag.of(e.getKey(), e.getValue()))
+            .toList());
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/metrics/NoopMetricsProvider.java b/src/main/java/com/uber/ugroup/metrics/NoopMetricsProvider.java
index 97f9db8..14d604c 100644
--- a/src/main/java/com/uber/ugroup/metrics/NoopMetricsProvider.java
+++ b/src/main/java/com/uber/ugroup/metrics/NoopMetricsProvider.java
@@ -18,40 +18,38 @@
 import java.util.Map;
 import java.util.function.Supplier;
 
-/**
- * No-op implementation of MetricsProvider for testing or when metrics are disabled.
- */
+/** No-op implementation of MetricsProvider for testing or when metrics are disabled. */
 public class NoopMetricsProvider implements MetricsProvider {
 
-    public static final NoopMetricsProvider INSTANCE = new NoopMetricsProvider();
-
-    @Override
-    public void incrementCounter(String name, Map tags) {
-        // no-op
-    }
-
-    @Override
-    public void incrementCounter(String name, long count, Map tags) {
-        // no-op
-    }
-
-    @Override
-    public void registerGauge(String name, Supplier valueSupplier, Map tags) {
-        // no-op
-    }
-
-    @Override
-    public void recordGauge(String name, double value, Map tags) {
-        // no-op
-    }
-
-    @Override
-    public void recordTimer(String name, long millis, Map tags) {
-        // no-op
-    }
-
-    @Override
-    public MetricsProvider tagged(Map tags) {
-        return this;
-    }
+  public static final NoopMetricsProvider INSTANCE = new NoopMetricsProvider();
+
+  @Override
+  public void incrementCounter(String name, Map tags) {
+    // no-op
+  }
+
+  @Override
+  public void incrementCounter(String name, long count, Map tags) {
+    // no-op
+  }
+
+  @Override
+  public void registerGauge(String name, Supplier valueSupplier, Map tags) {
+    // no-op
+  }
+
+  @Override
+  public void recordGauge(String name, double value, Map tags) {
+    // no-op
+  }
+
+  @Override
+  public void recordTimer(String name, long millis, Map tags) {
+    // no-op
+  }
+
+  @Override
+  public MetricsProvider tagged(Map tags) {
+    return this;
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/model/ConsumerLagState.java b/src/main/java/com/uber/ugroup/model/ConsumerLagState.java
index 8eb6d5b..3a10033 100644
--- a/src/main/java/com/uber/ugroup/model/ConsumerLagState.java
+++ b/src/main/java/com/uber/ugroup/model/ConsumerLagState.java
@@ -22,188 +22,181 @@
 import java.util.Optional;
 
 /**
- * Represents the lag state for a consumer group on a topic.
- * Contains per-partition lag information.
+ * Represents the lag state for a consumer group on a topic. Contains per-partition lag information.
  */
 public final class ConsumerLagState {
 
-    private final GroupAndTopic groupAndTopic;
-    private final Map partitionLagStates;
-    private final long computedAtMillis;
+  private final GroupAndTopic groupAndTopic;
+  private final Map partitionLagStates;
+  private final long computedAtMillis;
+
+  public ConsumerLagState(
+      GroupAndTopic groupAndTopic, Map partitionLagStates) {
+    this.groupAndTopic = groupAndTopic;
+    this.partitionLagStates = Collections.unmodifiableMap(new HashMap<>(partitionLagStates));
+    this.computedAtMillis = System.currentTimeMillis();
+  }
+
+  public GroupAndTopic getGroupAndTopic() {
+    return groupAndTopic;
+  }
+
+  public Map getPartitionLagStates() {
+    return partitionLagStates;
+  }
+
+  public long getComputedAtMillis() {
+    return computedAtMillis;
+  }
+
+  /** Get the total offset lag across all partitions. */
+  public long getTotalOffsetLag() {
+    return partitionLagStates.values().stream().mapToLong(PartitionLagState::getOffsetLag).sum();
+  }
+
+  /** Get the maximum time lag across all partitions. */
+  public Optional getMaxTimeLag() {
+    return partitionLagStates.values().stream()
+        .map(PartitionLagState::getTimeLag)
+        .filter(Optional::isPresent)
+        .map(Optional::get)
+        .max(Duration::compareTo);
+  }
+
+  /** Get the average percentage lag across all partitions. */
+  public double getAveragePercentageLag() {
+    return partitionLagStates.values().stream()
+        .mapToDouble(PartitionLagState::getPercentageLag)
+        .average()
+        .orElse(0.0);
+  }
+
+  /**
+   * Check if the freshness SLA is breached for any partition.
+   *
+   * @param sla the maximum acceptable time since last commit
+   * @return true if any partition exceeds the SLA
+   */
+  public boolean isFreshnessSLABreached(Duration sla) {
+    return partitionLagStates.values().stream()
+        .anyMatch(
+            state ->
+                state
+                    .getTimeSinceLastCommit()
+                    .map(duration -> duration.compareTo(sla) > 0)
+                    .orElse(false));
+  }
+
+  /** Represents lag state for a single partition. */
+  public static final class PartitionLagState {
+    private final int partition;
+    private final long committedOffset;
+    private final long endOffset;
+    private final long beginningOffset;
+    private final long offsetLag;
+    private final double percentageLag;
+    private final Duration timeLag;
+    private final Duration timeSinceLastCommit;
+
+    private PartitionLagState(Builder builder) {
+      this.partition = builder.partition;
+      this.committedOffset = builder.committedOffset;
+      this.endOffset = builder.endOffset;
+      this.beginningOffset = builder.beginningOffset;
+      this.offsetLag = builder.offsetLag;
+      this.percentageLag = builder.percentageLag;
+      this.timeLag = builder.timeLag;
+      this.timeSinceLastCommit = builder.timeSinceLastCommit;
+    }
+
+    public static Builder builder(int partition) {
+      return new Builder(partition);
+    }
 
-    public ConsumerLagState(GroupAndTopic groupAndTopic, Map partitionLagStates) {
-        this.groupAndTopic = groupAndTopic;
-        this.partitionLagStates = Collections.unmodifiableMap(new HashMap<>(partitionLagStates));
-        this.computedAtMillis = System.currentTimeMillis();
+    public int getPartition() {
+      return partition;
     }
 
-    public GroupAndTopic getGroupAndTopic() {
-        return groupAndTopic;
+    public long getCommittedOffset() {
+      return committedOffset;
     }
 
-    public Map getPartitionLagStates() {
-        return partitionLagStates;
+    public long getEndOffset() {
+      return endOffset;
     }
 
-    public long getComputedAtMillis() {
-        return computedAtMillis;
+    public long getBeginningOffset() {
+      return beginningOffset;
     }
 
-    /**
-     * Get the total offset lag across all partitions.
-     */
-    public long getTotalOffsetLag() {
-        return partitionLagStates.values().stream()
-                .mapToLong(PartitionLagState::getOffsetLag)
-                .sum();
+    public long getOffsetLag() {
+      return offsetLag;
     }
 
-    /**
-     * Get the maximum time lag across all partitions.
-     */
-    public Optional getMaxTimeLag() {
-        return partitionLagStates.values().stream()
-                .map(PartitionLagState::getTimeLag)
-                .filter(Optional::isPresent)
-                .map(Optional::get)
-                .max(Duration::compareTo);
+    public double getPercentageLag() {
+      return percentageLag;
     }
 
-    /**
-     * Get the average percentage lag across all partitions.
-     */
-    public double getAveragePercentageLag() {
-        return partitionLagStates.values().stream()
-                .mapToDouble(PartitionLagState::getPercentageLag)
-                .average()
-                .orElse(0.0);
+    public Optional getTimeLag() {
+      return Optional.ofNullable(timeLag);
     }
 
-    /**
-     * Check if the freshness SLA is breached for any partition.
-     *
-     * @param sla the maximum acceptable time since last commit
-     * @return true if any partition exceeds the SLA
-     */
-    public boolean isFreshnessSLABreached(Duration sla) {
-        return partitionLagStates.values().stream()
-                .anyMatch(state -> state.getTimeSinceLastCommit()
-                        .map(duration -> duration.compareTo(sla) > 0)
-                        .orElse(false));
+    public Optional getTimeSinceLastCommit() {
+      return Optional.ofNullable(timeSinceLastCommit);
     }
 
-    /**
-     * Represents lag state for a single partition.
-     */
-    public static final class PartitionLagState {
-        private final int partition;
-        private final long committedOffset;
-        private final long endOffset;
-        private final long beginningOffset;
-        private final long offsetLag;
-        private final double percentageLag;
-        private final Duration timeLag;
-        private final Duration timeSinceLastCommit;
-
-        private PartitionLagState(Builder builder) {
-            this.partition = builder.partition;
-            this.committedOffset = builder.committedOffset;
-            this.endOffset = builder.endOffset;
-            this.beginningOffset = builder.beginningOffset;
-            this.offsetLag = builder.offsetLag;
-            this.percentageLag = builder.percentageLag;
-            this.timeLag = builder.timeLag;
-            this.timeSinceLastCommit = builder.timeSinceLastCommit;
-        }
-
-        public static Builder builder(int partition) {
-            return new Builder(partition);
-        }
-
-        public int getPartition() {
-            return partition;
-        }
-
-        public long getCommittedOffset() {
-            return committedOffset;
-        }
-
-        public long getEndOffset() {
-            return endOffset;
-        }
-
-        public long getBeginningOffset() {
-            return beginningOffset;
-        }
-
-        public long getOffsetLag() {
-            return offsetLag;
-        }
-
-        public double getPercentageLag() {
-            return percentageLag;
-        }
-
-        public Optional getTimeLag() {
-            return Optional.ofNullable(timeLag);
-        }
-
-        public Optional getTimeSinceLastCommit() {
-            return Optional.ofNullable(timeSinceLastCommit);
-        }
-
-        public static class Builder {
-            private final int partition;
-            private long committedOffset;
-            private long endOffset;
-            private long beginningOffset;
-            private long offsetLag;
-            private double percentageLag;
-            private Duration timeLag;
-            private Duration timeSinceLastCommit;
-
-            public Builder(int partition) {
-                this.partition = partition;
-            }
-
-            public Builder committedOffset(long committedOffset) {
-                this.committedOffset = committedOffset;
-                return this;
-            }
-
-            public Builder endOffset(long endOffset) {
-                this.endOffset = endOffset;
-                return this;
-            }
-
-            public Builder beginningOffset(long beginningOffset) {
-                this.beginningOffset = beginningOffset;
-                return this;
-            }
-
-            public Builder offsetLag(long offsetLag) {
-                this.offsetLag = offsetLag;
-                return this;
-            }
-
-            public Builder percentageLag(double percentageLag) {
-                this.percentageLag = percentageLag;
-                return this;
-            }
-
-            public Builder timeLag(Duration timeLag) {
-                this.timeLag = timeLag;
-                return this;
-            }
-
-            public Builder timeSinceLastCommit(Duration timeSinceLastCommit) {
-                this.timeSinceLastCommit = timeSinceLastCommit;
-                return this;
-            }
-
-            public PartitionLagState build() {
-                return new PartitionLagState(this);
-            }
-        }
+    public static class Builder {
+      private final int partition;
+      private long committedOffset;
+      private long endOffset;
+      private long beginningOffset;
+      private long offsetLag;
+      private double percentageLag;
+      private Duration timeLag;
+      private Duration timeSinceLastCommit;
+
+      public Builder(int partition) {
+        this.partition = partition;
+      }
+
+      public Builder committedOffset(long committedOffset) {
+        this.committedOffset = committedOffset;
+        return this;
+      }
+
+      public Builder endOffset(long endOffset) {
+        this.endOffset = endOffset;
+        return this;
+      }
+
+      public Builder beginningOffset(long beginningOffset) {
+        this.beginningOffset = beginningOffset;
+        return this;
+      }
+
+      public Builder offsetLag(long offsetLag) {
+        this.offsetLag = offsetLag;
+        return this;
+      }
+
+      public Builder percentageLag(double percentageLag) {
+        this.percentageLag = percentageLag;
+        return this;
+      }
+
+      public Builder timeLag(Duration timeLag) {
+        this.timeLag = timeLag;
+        return this;
+      }
+
+      public Builder timeSinceLastCommit(Duration timeSinceLastCommit) {
+        this.timeSinceLastCommit = timeSinceLastCommit;
+        return this;
+      }
+
+      public PartitionLagState build() {
+        return new PartitionLagState(this);
+      }
     }
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/model/ConsumerMetadata.java b/src/main/java/com/uber/ugroup/model/ConsumerMetadata.java
index 8790c70..d7e99c8 100644
--- a/src/main/java/com/uber/ugroup/model/ConsumerMetadata.java
+++ b/src/main/java/com/uber/ugroup/model/ConsumerMetadata.java
@@ -18,84 +18,88 @@
 import java.util.Map;
 import java.util.Optional;
 
-/**
- * Metadata about a consumer, typically provided by watchlist providers.
- */
+/** Metadata about a consumer, typically provided by watchlist providers. */
 public final class ConsumerMetadata {
 
-    private final String consumerType;
-    private final String cluster;
-    private final Map additionalTags;
-    private final boolean enabled;
-
-    private ConsumerMetadata(Builder builder) {
-        this.consumerType = builder.consumerType;
-        this.cluster = builder.cluster;
-        this.additionalTags = builder.additionalTags != null ? Map.copyOf(builder.additionalTags) : Map.of();
-        this.enabled = builder.enabled;
-    }
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    public String getConsumerType() {
-        return consumerType;
-    }
-
-    public String getCluster() {
-        return cluster;
-    }
-
-    public Map getAdditionalTags() {
-        return additionalTags;
+  private final String consumerType;
+  private final String cluster;
+  private final Map additionalTags;
+  private final boolean enabled;
+
+  private ConsumerMetadata(Builder builder) {
+    this.consumerType = builder.consumerType;
+    this.cluster = builder.cluster;
+    this.additionalTags =
+        builder.additionalTags != null ? Map.copyOf(builder.additionalTags) : Map.of();
+    this.enabled = builder.enabled;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public String getConsumerType() {
+    return consumerType;
+  }
+
+  public String getCluster() {
+    return cluster;
+  }
+
+  public Map getAdditionalTags() {
+    return additionalTags;
+  }
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public Optional getTag(String key) {
+    return Optional.ofNullable(additionalTags.get(key));
+  }
+
+  public static class Builder {
+    private String consumerType = "default";
+    private String cluster;
+    private Map additionalTags;
+    private boolean enabled = true;
+
+    public Builder consumerType(String consumerType) {
+      this.consumerType = consumerType;
+      return this;
     }
 
-    public boolean isEnabled() {
-        return enabled;
+    public Builder cluster(String cluster) {
+      this.cluster = cluster;
+      return this;
     }
 
-    public Optional getTag(String key) {
-        return Optional.ofNullable(additionalTags.get(key));
+    public Builder additionalTags(Map additionalTags) {
+      this.additionalTags = additionalTags;
+      return this;
     }
 
-    public static class Builder {
-        private String consumerType = "default";
-        private String cluster;
-        private Map additionalTags;
-        private boolean enabled = true;
-
-        public Builder consumerType(String consumerType) {
-            this.consumerType = consumerType;
-            return this;
-        }
-
-        public Builder cluster(String cluster) {
-            this.cluster = cluster;
-            return this;
-        }
-
-        public Builder additionalTags(Map additionalTags) {
-            this.additionalTags = additionalTags;
-            return this;
-        }
-
-        public Builder enabled(boolean enabled) {
-            this.enabled = enabled;
-            return this;
-        }
-
-        public ConsumerMetadata build() {
-            return new ConsumerMetadata(this);
-        }
+    public Builder enabled(boolean enabled) {
+      this.enabled = enabled;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "ConsumerMetadata{" +
-                "consumerType='" + consumerType + '\'' +
-                ", cluster='" + cluster + '\'' +
-                ", enabled=" + enabled +
-                '}';
+    public ConsumerMetadata build() {
+      return new ConsumerMetadata(this);
     }
+  }
+
+  @Override
+  public String toString() {
+    return "ConsumerMetadata{"
+        + "consumerType='"
+        + consumerType
+        + '\''
+        + ", cluster='"
+        + cluster
+        + '\''
+        + ", enabled="
+        + enabled
+        + '}';
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/model/GroupAndTopic.java b/src/main/java/com/uber/ugroup/model/GroupAndTopic.java
index 55eebe3..b9fa634 100644
--- a/src/main/java/com/uber/ugroup/model/GroupAndTopic.java
+++ b/src/main/java/com/uber/ugroup/model/GroupAndTopic.java
@@ -18,81 +18,78 @@
 import java.util.Objects;
 
 /**
- * Represents a consumer group and topic combination.
- * This is the primary unit of monitoring in uGroup.
+ * Represents a consumer group and topic combination. This is the primary unit of monitoring in
+ * uGroup.
  */
 public final class GroupAndTopic {
 
-    private final String group;
-    private final String topic;
-    private final int consumerOffsetsPartition;
+  private final String group;
+  private final String topic;
+  private final int consumerOffsetsPartition;
 
-    public GroupAndTopic(String group, String topic) {
-        this.group = Objects.requireNonNull(group, "group cannot be null");
-        this.topic = Objects.requireNonNull(topic, "topic cannot be null");
-        this.consumerOffsetsPartition = calculatePartition(group);
-    }
+  public GroupAndTopic(String group, String topic) {
+    this.group = Objects.requireNonNull(group, "group cannot be null");
+    this.topic = Objects.requireNonNull(topic, "topic cannot be null");
+    this.consumerOffsetsPartition = calculatePartition(group);
+  }
 
-    public GroupAndTopic(String group, String topic, int consumerOffsetsPartition) {
-        this.group = Objects.requireNonNull(group, "group cannot be null");
-        this.topic = Objects.requireNonNull(topic, "topic cannot be null");
-        this.consumerOffsetsPartition = consumerOffsetsPartition;
-    }
+  public GroupAndTopic(String group, String topic, int consumerOffsetsPartition) {
+    this.group = Objects.requireNonNull(group, "group cannot be null");
+    this.topic = Objects.requireNonNull(topic, "topic cannot be null");
+    this.consumerOffsetsPartition = consumerOffsetsPartition;
+  }
 
-    /**
-     * Calculate which partition of __consumer_offsets this group's commits go to.
-     * This uses the same algorithm as Kafka: abs(groupId.hashCode) % numPartitions.
-     * Default numPartitions is 50.
-     */
-    public static int calculatePartition(String groupId) {
-        return calculatePartition(groupId, 50);
-    }
+  /**
+   * Calculate which partition of __consumer_offsets this group's commits go to. This uses the same
+   * algorithm as Kafka: abs(groupId.hashCode) % numPartitions. Default numPartitions is 50.
+   */
+  public static int calculatePartition(String groupId) {
+    return calculatePartition(groupId, 50);
+  }
 
-    /**
-     * Calculate which partition of __consumer_offsets this group's commits go to.
-     *
-     * @param groupId       the consumer group ID
-     * @param numPartitions the number of partitions in __consumer_offsets
-     * @return the partition number
-     */
-    public static int calculatePartition(String groupId, int numPartitions) {
-        return Math.abs(groupId.hashCode() % numPartitions);
-    }
+  /**
+   * Calculate which partition of __consumer_offsets this group's commits go to.
+   *
+   * @param groupId the consumer group ID
+   * @param numPartitions the number of partitions in __consumer_offsets
+   * @return the partition number
+   */
+  public static int calculatePartition(String groupId, int numPartitions) {
+    return Math.abs(groupId.hashCode() % numPartitions);
+  }
 
-    public String getGroup() {
-        return group;
-    }
+  public String getGroup() {
+    return group;
+  }
 
-    public String getTopic() {
-        return topic;
-    }
+  public String getTopic() {
+    return topic;
+  }
 
-    public int getConsumerOffsetsPartition() {
-        return consumerOffsetsPartition;
-    }
+  public int getConsumerOffsetsPartition() {
+    return consumerOffsetsPartition;
+  }
 
-    /**
-     * Create a new GroupAndTopic with an updated partition count.
-     */
-    public GroupAndTopic withPartitionCount(int numPartitions) {
-        return new GroupAndTopic(group, topic, calculatePartition(group, numPartitions));
-    }
+  /** Create a new GroupAndTopic with an updated partition count. */
+  public GroupAndTopic withPartitionCount(int numPartitions) {
+    return new GroupAndTopic(group, topic, calculatePartition(group, numPartitions));
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        GroupAndTopic that = (GroupAndTopic) o;
-        return Objects.equals(group, that.group) && Objects.equals(topic, that.topic);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    GroupAndTopic that = (GroupAndTopic) o;
+    return Objects.equals(group, that.group) && Objects.equals(topic, that.topic);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(group, topic);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(group, topic);
+  }
 
-    @Override
-    public String toString() {
-        return "GroupAndTopic{group='" + group + "', topic='" + topic + "'}";
-    }
+  @Override
+  public String toString() {
+    return "GroupAndTopic{group='" + group + "', topic='" + topic + "'}";
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/model/LastCommittedOffset.java b/src/main/java/com/uber/ugroup/model/LastCommittedOffset.java
index 8364238..34cf1a7 100644
--- a/src/main/java/com/uber/ugroup/model/LastCommittedOffset.java
+++ b/src/main/java/com/uber/ugroup/model/LastCommittedOffset.java
@@ -15,45 +15,41 @@
  */
 package com.uber.ugroup.model;
 
-/**
- * Represents the last committed offset for a partition, including timestamp.
- */
+/** Represents the last committed offset for a partition, including timestamp. */
 public final class LastCommittedOffset {
 
-    private final long offset;
-    private final long commitTimeMillis;
-
-    public LastCommittedOffset(long offset, long commitTimeMillis) {
-        this.offset = offset;
-        this.commitTimeMillis = commitTimeMillis;
-    }
-
-    /**
-     * Create a new LastCommittedOffset with current time.
-     */
-    public static LastCommittedOffset now(long offset) {
-        return new LastCommittedOffset(offset, System.currentTimeMillis());
-    }
-
-    public long getOffset() {
-        return offset;
-    }
-
-    public long getCommitTimeMillis() {
-        return commitTimeMillis;
-    }
-
-    /**
-     * Get the time elapsed since this offset was committed.
-     *
-     * @return milliseconds since commit
-     */
-    public long getTimeSinceCommitMillis() {
-        return System.currentTimeMillis() - commitTimeMillis;
-    }
-
-    @Override
-    public String toString() {
-        return "LastCommittedOffset{offset=" + offset + ", commitTimeMillis=" + commitTimeMillis + "}";
-    }
+  private final long offset;
+  private final long commitTimeMillis;
+
+  public LastCommittedOffset(long offset, long commitTimeMillis) {
+    this.offset = offset;
+    this.commitTimeMillis = commitTimeMillis;
+  }
+
+  /** Create a new LastCommittedOffset with current time. */
+  public static LastCommittedOffset now(long offset) {
+    return new LastCommittedOffset(offset, System.currentTimeMillis());
+  }
+
+  public long getOffset() {
+    return offset;
+  }
+
+  public long getCommitTimeMillis() {
+    return commitTimeMillis;
+  }
+
+  /**
+   * Get the time elapsed since this offset was committed.
+   *
+   * @return milliseconds since commit
+   */
+  public long getTimeSinceCommitMillis() {
+    return System.currentTimeMillis() - commitTimeMillis;
+  }
+
+  @Override
+  public String toString() {
+    return "LastCommittedOffset{offset=" + offset + ", commitTimeMillis=" + commitTimeMillis + "}";
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/processor/BaseCompactedOffsetsProcessor.java b/src/main/java/com/uber/ugroup/processor/BaseCompactedOffsetsProcessor.java
index c0fdb54..3c7d430 100644
--- a/src/main/java/com/uber/ugroup/processor/BaseCompactedOffsetsProcessor.java
+++ b/src/main/java/com/uber/ugroup/processor/BaseCompactedOffsetsProcessor.java
@@ -21,192 +21,193 @@
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
 import com.uber.ugroup.watchlist.WatchListProvider;
-import org.apache.kafka.clients.consumer.OffsetAndMetadata;
-import org.apache.kafka.common.PartitionInfo;
-import org.apache.kafka.common.TopicPartition;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.time.Duration;
 import java.time.Instant;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import org.apache.kafka.clients.consumer.OffsetAndMetadata;
+import org.apache.kafka.common.PartitionInfo;
+import org.apache.kafka.common.TopicPartition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-/**
- * Base implementation of CompactedOffsetsProcessor that calculates and reports consumer lag.
- */
+/** Base implementation of CompactedOffsetsProcessor that calculates and reports consumer lag. */
 public class BaseCompactedOffsetsProcessor implements CompactedOffsetsProcessor {
 
-    private static final Logger logger = LoggerFactory.getLogger(BaseCompactedOffsetsProcessor.class);
-    private static final long STUCK_PARTITION_THRESHOLD_MS = 300_000; // 5 minutes
-
-    private final MetricsProvider metricsProvider;
-    private final OffsetFetcher offsetFetcher;
-    private final WatchListProvider watchListProvider;
-    private final Map consumerLagStateCache;
-    private final String clusterName;
-
-    public BaseCompactedOffsetsProcessor(
-            MetricsProvider metricsProvider,
-            OffsetFetcher offsetFetcher,
-            WatchListProvider watchListProvider,
-            String clusterName) {
-        this.metricsProvider = metricsProvider;
-        this.offsetFetcher = offsetFetcher;
-        this.watchListProvider = watchListProvider;
-        this.clusterName = clusterName;
-        this.consumerLagStateCache = new ConcurrentHashMap<>();
+  private static final Logger logger = LoggerFactory.getLogger(BaseCompactedOffsetsProcessor.class);
+  private static final long STUCK_PARTITION_THRESHOLD_MS = 300_000; // 5 minutes
+
+  private final MetricsProvider metricsProvider;
+  private final OffsetFetcher offsetFetcher;
+  private final WatchListProvider watchListProvider;
+  private final Map consumerLagStateCache;
+  private final String clusterName;
+
+  public BaseCompactedOffsetsProcessor(
+      MetricsProvider metricsProvider,
+      OffsetFetcher offsetFetcher,
+      WatchListProvider watchListProvider,
+      String clusterName) {
+    this.metricsProvider = metricsProvider;
+    this.offsetFetcher = offsetFetcher;
+    this.watchListProvider = watchListProvider;
+    this.clusterName = clusterName;
+    this.consumerLagStateCache = new ConcurrentHashMap<>();
+  }
+
+  @Override
+  public boolean process(
+      GroupAndTopic groupAndTopic, Map lastCommittedOffsets) {
+    if (!watchListProvider.contains(groupAndTopic)) {
+      return false;
     }
 
-    @Override
-    public boolean process(GroupAndTopic groupAndTopic, Map lastCommittedOffsets) {
-        if (!watchListProvider.contains(groupAndTopic)) {
-            return false;
-        }
-
-        try {
-            reportConsumerGroupLag(groupAndTopic, lastCommittedOffsets);
-            return true;
-        } catch (Exception e) {
-            logger.error("Error processing lag for {}", groupAndTopic, e);
-            metricsProvider.incrementCounter("ugroup.processor.errors",
-                    Map.of("group", groupAndTopic.getGroup(), "topic", groupAndTopic.getTopic()));
-            return true; // Still claim ownership to prevent other processors
-        }
+    try {
+      reportConsumerGroupLag(groupAndTopic, lastCommittedOffsets);
+      return true;
+    } catch (Exception e) {
+      logger.error("Error processing lag for {}", groupAndTopic, e);
+      metricsProvider.incrementCounter(
+          "ugroup.processor.errors",
+          Map.of("group", groupAndTopic.getGroup(), "topic", groupAndTopic.getTopic()));
+      return true; // Still claim ownership to prevent other processors
+    }
+  }
+
+  private void reportConsumerGroupLag(
+      GroupAndTopic groupAndTopic, Map lastCommittedOffsets) {
+    String topic = groupAndTopic.getTopic();
+    String group = groupAndTopic.getGroup();
+
+    List partitions = offsetFetcher.partitionsFor(topic);
+    if (partitions == null || partitions.isEmpty()) {
+      metricsProvider.incrementCounter(
+          "ugroup.report.skipped", Map.of("reason", "no_partitions", "topic", topic));
+      return;
     }
 
-    private void reportConsumerGroupLag(GroupAndTopic groupAndTopic,
-                                         Map lastCommittedOffsets) {
-        String topic = groupAndTopic.getTopic();
-        String group = groupAndTopic.getGroup();
-
-        List partitions = offsetFetcher.partitionsFor(topic);
-        if (partitions == null || partitions.isEmpty()) {
-            metricsProvider.incrementCounter("ugroup.report.skipped",
-                    Map.of("reason", "no_partitions", "topic", topic));
-            return;
-        }
+    List topicPartitions =
+        partitions.stream().map(pi -> new TopicPartition(topic, pi.partition())).toList();
 
-        List topicPartitions = partitions.stream()
-                .map(pi -> new TopicPartition(topic, pi.partition()))
-                .toList();
+    Map beginningOffsets = offsetFetcher.beginningOffsets(topicPartitions);
+    Map endOffsets = offsetFetcher.endOffsets(topicPartitions);
 
-        Map beginningOffsets = offsetFetcher.beginningOffsets(topicPartitions);
-        Map endOffsets = offsetFetcher.endOffsets(topicPartitions);
+    if (beginningOffsets.isEmpty() || endOffsets.isEmpty()) {
+      metricsProvider.incrementCounter(
+          "ugroup.report.skipped", Map.of("reason", "empty_offsets", "topic", topic));
+      return;
+    }
 
-        if (beginningOffsets.isEmpty() || endOffsets.isEmpty()) {
-            metricsProvider.incrementCounter("ugroup.report.skipped",
-                    Map.of("reason", "empty_offsets", "topic", topic));
-            return;
+    // Build complete committed offsets map, fetching from broker for missing partitions
+    Map completeOffsets =
+        buildCompleteLastCommittedOffsets(groupAndTopic, lastCommittedOffsets, endOffsets);
+
+    Map partitionStates = new HashMap<>();
+
+    for (TopicPartition tp : topicPartitions) {
+      int partition = tp.partition();
+      long beginningOffset = beginningOffsets.getOrDefault(tp, 0L);
+      long endOffset = endOffsets.getOrDefault(tp, 0L);
+
+      LastCommittedOffset lastCommitted = completeOffsets.get(partition);
+      long committedOffset = lastCommitted != null ? lastCommitted.getOffset() : -1L;
+
+      // Calculate lag
+      long offsetLag =
+          LagCalculationUtils.getConsumerLag(beginningOffset, committedOffset, endOffset);
+      double percentageLag =
+          LagCalculationUtils.getConsumerLagPercentage(beginningOffset, committedOffset, endOffset);
+
+      Map tags =
+          Map.of(
+              "cluster", clusterName,
+              "topic", topic,
+              "group", group,
+              "partition", String.valueOf(partition),
+              "watchlist", watchListProvider.getName());
+
+      // Report metrics
+      metricsProvider.recordGauge("ugroup.consumer_lag", offsetLag, tags);
+      metricsProvider.recordGauge("ugroup.consumer_lag_percentage", percentageLag, tags);
+      metricsProvider.recordGauge("ugroup.committed_offset", committedOffset, tags);
+
+      // Report stuck partition
+      if (lastCommitted != null) {
+        long timeSinceCommit = lastCommitted.getTimeSinceCommitMillis();
+        if (timeSinceCommit > STUCK_PARTITION_THRESHOLD_MS && offsetLag > 0) {
+          metricsProvider.recordGauge("ugroup.stuck_partition", timeSinceCommit, tags);
         }
 
-        // Build complete committed offsets map, fetching from broker for missing partitions
-        Map completeOffsets =
-                buildCompleteLastCommittedOffsets(groupAndTopic, lastCommittedOffsets, endOffsets);
-
-        Map partitionStates = new HashMap<>();
-
-        for (TopicPartition tp : topicPartitions) {
-            int partition = tp.partition();
-            long beginningOffset = beginningOffsets.getOrDefault(tp, 0L);
-            long endOffset = endOffsets.getOrDefault(tp, 0L);
-
-            LastCommittedOffset lastCommitted = completeOffsets.get(partition);
-            long committedOffset = lastCommitted != null ? lastCommitted.getOffset() : -1L;
-
-            // Calculate lag
-            long offsetLag = LagCalculationUtils.getConsumerLag(beginningOffset, committedOffset, endOffset);
-            double percentageLag = LagCalculationUtils.getConsumerLagPercentage(beginningOffset, committedOffset, endOffset);
-
-            Map tags = Map.of(
-                    "cluster", clusterName,
-                    "topic", topic,
-                    "group", group,
-                    "partition", String.valueOf(partition),
-                    "watchlist", watchListProvider.getName());
-
-            // Report metrics
-            metricsProvider.recordGauge("ugroup.consumer_lag", offsetLag, tags);
-            metricsProvider.recordGauge("ugroup.consumer_lag_percentage", percentageLag, tags);
-            metricsProvider.recordGauge("ugroup.committed_offset", committedOffset, tags);
-
-            // Report stuck partition
-            if (lastCommitted != null) {
-                long timeSinceCommit = lastCommitted.getTimeSinceCommitMillis();
-                if (timeSinceCommit > STUCK_PARTITION_THRESHOLD_MS && offsetLag > 0) {
-                    metricsProvider.recordGauge("ugroup.stuck_partition", timeSinceCommit, tags);
-                }
-
-                // Time-based lag (approximation)
-                if (offsetLag == 0) {
-                    metricsProvider.recordGauge("ugroup.consumer_lag_seconds", 0, tags);
-                } else if (committedOffset > beginningOffset) {
-                    // Approximate time lag based on commit staleness
-                    metricsProvider.recordGauge("ugroup.consumer_lag_seconds",
-                            timeSinceCommit / 1000.0, tags);
-                }
-            }
-
-            // Build partition state
-            ConsumerLagState.PartitionLagState.Builder stateBuilder =
-                    ConsumerLagState.PartitionLagState.builder(partition)
-                            .committedOffset(committedOffset)
-                            .beginningOffset(beginningOffset)
-                            .endOffset(endOffset)
-                            .offsetLag(offsetLag)
-                            .percentageLag(percentageLag);
-
-            if (lastCommitted != null) {
-                stateBuilder.timeSinceLastCommit(Duration.ofMillis(lastCommitted.getTimeSinceCommitMillis()));
-            }
-
-            partitionStates.put(partition, stateBuilder.build());
+        // Time-based lag (approximation)
+        if (offsetLag == 0) {
+          metricsProvider.recordGauge("ugroup.consumer_lag_seconds", 0, tags);
+        } else if (committedOffset > beginningOffset) {
+          // Approximate time lag based on commit staleness
+          metricsProvider.recordGauge(
+              "ugroup.consumer_lag_seconds", timeSinceCommit / 1000.0, tags);
         }
-
-        // Store lag state for potential external access
-        consumerLagStateCache.put(groupAndTopic, new ConsumerLagState(groupAndTopic, partitionStates));
+      }
+
+      // Build partition state
+      ConsumerLagState.PartitionLagState.Builder stateBuilder =
+          ConsumerLagState.PartitionLagState.builder(partition)
+              .committedOffset(committedOffset)
+              .beginningOffset(beginningOffset)
+              .endOffset(endOffset)
+              .offsetLag(offsetLag)
+              .percentageLag(percentageLag);
+
+      if (lastCommitted != null) {
+        stateBuilder.timeSinceLastCommit(
+            Duration.ofMillis(lastCommitted.getTimeSinceCommitMillis()));
+      }
+
+      partitionStates.put(partition, stateBuilder.build());
     }
 
-    private Map buildCompleteLastCommittedOffsets(
-            GroupAndTopic groupAndTopic,
-            Map lastCommittedOffsets,
-            Map endOffsets) {
-
-        Map complete = new HashMap<>(lastCommittedOffsets);
-
-        // Find missing partitions and fetch from broker
-        for (TopicPartition tp : endOffsets.keySet()) {
-            int partition = tp.partition();
-            if (!complete.containsKey(partition) && endOffsets.get(tp) > 0) {
-                // Need to fetch committed offset from broker
-                try {
-                    Map groupOffsets =
-                            offsetFetcher.listConsumerGroupOffsets(groupAndTopic.getGroup());
-
-                    OffsetAndMetadata oam = groupOffsets.get(tp);
-                    if (oam != null) {
-                        complete.put(partition, new LastCommittedOffset(oam.offset(), Instant.now().toEpochMilli()));
-                    } else {
-                        // Consumer never committed for this partition
-                        complete.put(partition, new LastCommittedOffset(-1, 0));
-                    }
-                } catch (Exception e) {
-                    logger.warn("Failed to fetch committed offset for {} partition {}",
-                            groupAndTopic, partition, e);
-                    complete.put(partition, new LastCommittedOffset(-1, 0));
-                }
-            }
-        }
+    // Store lag state for potential external access
+    consumerLagStateCache.put(groupAndTopic, new ConsumerLagState(groupAndTopic, partitionStates));
+  }
 
-        return complete;
-    }
+  private Map buildCompleteLastCommittedOffsets(
+      GroupAndTopic groupAndTopic,
+      Map lastCommittedOffsets,
+      Map endOffsets) {
+
+    Map complete = new HashMap<>(lastCommittedOffsets);
 
-    /**
-     * Get cached lag state for a group-topic.
-     */
-    public ConsumerLagState getLagState(GroupAndTopic groupAndTopic) {
-        return consumerLagStateCache.get(groupAndTopic);
+    // Find missing partitions and fetch from broker
+    for (TopicPartition tp : endOffsets.keySet()) {
+      int partition = tp.partition();
+      if (!complete.containsKey(partition) && endOffsets.get(tp) > 0) {
+        // Need to fetch committed offset from broker
+        try {
+          Map groupOffsets =
+              offsetFetcher.listConsumerGroupOffsets(groupAndTopic.getGroup());
+
+          OffsetAndMetadata oam = groupOffsets.get(tp);
+          if (oam != null) {
+            complete.put(
+                partition, new LastCommittedOffset(oam.offset(), Instant.now().toEpochMilli()));
+          } else {
+            // Consumer never committed for this partition
+            complete.put(partition, new LastCommittedOffset(-1, 0));
+          }
+        } catch (Exception e) {
+          logger.warn(
+              "Failed to fetch committed offset for {} partition {}", groupAndTopic, partition, e);
+          complete.put(partition, new LastCommittedOffset(-1, 0));
+        }
+      }
     }
+
+    return complete;
+  }
+
+  /** Get cached lag state for a group-topic. */
+  public ConsumerLagState getLagState(GroupAndTopic groupAndTopic) {
+    return consumerLagStateCache.get(groupAndTopic);
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/processor/CommittedOffsetsCompactor.java b/src/main/java/com/uber/ugroup/processor/CommittedOffsetsCompactor.java
index f808fb8..5c47850 100644
--- a/src/main/java/com/uber/ugroup/processor/CommittedOffsetsCompactor.java
+++ b/src/main/java/com/uber/ugroup/processor/CommittedOffsetsCompactor.java
@@ -22,11 +22,6 @@
 import com.uber.ugroup.processor.state.LastCommittedOffsetState;
 import com.uber.ugroup.watchlist.BlockList;
 import com.uber.ugroup.watchlist.WatchListProvider;
-import org.apache.kafka.clients.consumer.OffsetAndMetadata;
-import org.apache.kafka.common.TopicPartition;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -41,229 +36,231 @@
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import org.apache.kafka.clients.consumer.OffsetAndMetadata;
+import org.apache.kafka.common.TopicPartition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
- * Compacts committed offsets from per-partition messages to per group-topic events.
- * Runs periodic compaction to process all watched consumers.
+ * Compacts committed offsets from per-partition messages to per group-topic events. Runs periodic
+ * compaction to process all watched consumers.
  */
 public class CommittedOffsetsCompactor implements AutoCloseable {
 
-    private static final Logger logger = LoggerFactory.getLogger(CommittedOffsetsCompactor.class);
-    private static final int MAX_QUEUED_TASKS = 200;
-
-    private final MetricsProvider metricsProvider;
-    private final OffsetFetcher offsetFetcher;
-    private final BlockList blockList;
-    private final List watchListProviders;
-    private final CompactorEventCallback eventCallback;
-
-    private final LastCommittedOffsetState lastCommittedOffsetState;
-    private final ConcurrentHashMap groupAndTopicsInPipeline;
-    private final AtomicReference> consumerOffsetsTopicPartitions;
-
-    private final ExecutorService executor;
-    private final BlockingQueue queuedTasks;
-    private final ScheduledExecutorService scheduler;
-
-    private final int consumerOffsetsPartitionCount;
-    private final long compactIntervalMillis;
-    private final long compactingDelayMillis;
-
-    public CommittedOffsetsCompactor(
-            MetricsProvider metricsProvider,
-            OffsetFetcher offsetFetcher,
-            BlockList blockList,
-            List watchListProviders,
-            CompactorEventCallback eventCallback,
-            int parallelism,
-            int consumerOffsetsPartitionCount,
-            long compactIntervalMillis,
-            long compactingDelayMillis) {
-
-        this.metricsProvider = metricsProvider;
-        this.offsetFetcher = offsetFetcher;
-        this.blockList = blockList;
-        this.watchListProviders = watchListProviders;
-        this.eventCallback = eventCallback;
-        this.consumerOffsetsPartitionCount = consumerOffsetsPartitionCount;
-        this.compactIntervalMillis = compactIntervalMillis;
-        this.compactingDelayMillis = compactingDelayMillis;
-
-        this.lastCommittedOffsetState = new LastCommittedOffsetState(metricsProvider);
-        this.groupAndTopicsInPipeline = new ConcurrentHashMap<>();
-        this.consumerOffsetsTopicPartitions = new AtomicReference<>(new HashSet<>());
-
-        this.queuedTasks = new LinkedBlockingDeque<>(MAX_QUEUED_TASKS);
-        this.executor = new ThreadPoolExecutor(
-                parallelism,
-                parallelism,
-                Long.MAX_VALUE,
-                TimeUnit.SECONDS,
-                queuedTasks,
-                new ThreadPoolExecutor.CallerRunsPolicy());
-
-        this.scheduler = Executors.newScheduledThreadPool(1);
-        scheduler.scheduleAtFixedRate(
-                this::compactCommittedOffsets,
-                compactIntervalMillis,
-                compactIntervalMillis,
-                TimeUnit.MILLISECONDS);
-
-        logger.info("CommittedOffsetsCompactor started with compactInterval={}ms, parallelism={}",
-                compactIntervalMillis, parallelism);
+  private static final Logger logger = LoggerFactory.getLogger(CommittedOffsetsCompactor.class);
+  private static final int MAX_QUEUED_TASKS = 200;
+
+  private final MetricsProvider metricsProvider;
+  private final OffsetFetcher offsetFetcher;
+  private final BlockList blockList;
+  private final List watchListProviders;
+  private final CompactorEventCallback eventCallback;
+
+  private final LastCommittedOffsetState lastCommittedOffsetState;
+  private final ConcurrentHashMap groupAndTopicsInPipeline;
+  private final AtomicReference> consumerOffsetsTopicPartitions;
+
+  private final ExecutorService executor;
+  private final BlockingQueue queuedTasks;
+  private final ScheduledExecutorService scheduler;
+
+  private final int consumerOffsetsPartitionCount;
+  private final long compactIntervalMillis;
+  private final long compactingDelayMillis;
+
+  public CommittedOffsetsCompactor(
+      MetricsProvider metricsProvider,
+      OffsetFetcher offsetFetcher,
+      BlockList blockList,
+      List watchListProviders,
+      CompactorEventCallback eventCallback,
+      int parallelism,
+      int consumerOffsetsPartitionCount,
+      long compactIntervalMillis,
+      long compactingDelayMillis) {
+
+    this.metricsProvider = metricsProvider;
+    this.offsetFetcher = offsetFetcher;
+    this.blockList = blockList;
+    this.watchListProviders = watchListProviders;
+    this.eventCallback = eventCallback;
+    this.consumerOffsetsPartitionCount = consumerOffsetsPartitionCount;
+    this.compactIntervalMillis = compactIntervalMillis;
+    this.compactingDelayMillis = compactingDelayMillis;
+
+    this.lastCommittedOffsetState = new LastCommittedOffsetState(metricsProvider);
+    this.groupAndTopicsInPipeline = new ConcurrentHashMap<>();
+    this.consumerOffsetsTopicPartitions = new AtomicReference<>(new HashSet<>());
+
+    this.queuedTasks = new LinkedBlockingDeque<>(MAX_QUEUED_TASKS);
+    this.executor =
+        new ThreadPoolExecutor(
+            parallelism,
+            parallelism,
+            Long.MAX_VALUE,
+            TimeUnit.SECONDS,
+            queuedTasks,
+            new ThreadPoolExecutor.CallerRunsPolicy());
+
+    this.scheduler = Executors.newScheduledThreadPool(1);
+    scheduler.scheduleAtFixedRate(
+        this::compactCommittedOffsets,
+        compactIntervalMillis,
+        compactIntervalMillis,
+        TimeUnit.MILLISECONDS);
+
+    logger.info(
+        "CommittedOffsetsCompactor started with compactInterval={}ms, parallelism={}",
+        compactIntervalMillis,
+        parallelism);
+  }
+
+  /** Record an offset commit event. */
+  public void offsetCommitted(String group, String topic, int partition, long offset) {
+    lastCommittedOffsetState.updateLastCommittedOffset(
+        group, topic, partition, offset, System.currentTimeMillis());
+  }
+
+  /** Add group-topic combinations to be compacted. */
+  public void addGroupAndTopicsToCompact(Set groupAndTopics, long timestamp) {
+    if (groupAndTopics == null) {
+      return;
     }
+    metricsProvider.incrementCounter(
+        "ugroup.compactor.tasks_submitted", groupAndTopics.size(), Map.of());
 
-    /**
-     * Record an offset commit event.
-     */
-    public void offsetCommitted(String group, String topic, int partition, long offset) {
-        lastCommittedOffsetState.updateLastCommittedOffset(
-                group, topic, partition, offset, System.currentTimeMillis());
+    for (GroupAndTopic gat : groupAndTopics) {
+      if (!blockList.isBlocked(gat)) {
+        groupAndTopicsInPipeline.putIfAbsent(gat, timestamp);
+      }
     }
-
-    /**
-     * Add group-topic combinations to be compacted.
-     */
-    public void addGroupAndTopicsToCompact(Set groupAndTopics, long timestamp) {
-        if (groupAndTopics == null) {
-            return;
-        }
-        metricsProvider.incrementCounter("ugroup.compactor.tasks_submitted",
-                groupAndTopics.size(), Map.of());
-
-        for (GroupAndTopic gat : groupAndTopics) {
-            if (!blockList.isBlocked(gat)) {
-                groupAndTopicsInPipeline.putIfAbsent(gat, timestamp);
-            }
-        }
+  }
+
+  /** Update the partitions of __consumer_offsets assigned to this processor. */
+  public void updatePartitionAssigned(Set partitions) {
+    consumerOffsetsTopicPartitions.set(new HashSet<>(partitions));
+    lastCommittedOffsetState.invalidateUnassignedEntries(partitions, consumerOffsetsPartitionCount);
+  }
+
+  private void compactCommittedOffsets() {
+    long startTime = System.currentTimeMillis();
+    try {
+      compactForGroupAndTopicsWatchList(startTime);
+    } catch (Exception e) {
+      logger.error("Error during offset compaction", e);
+      metricsProvider.incrementCounter("ugroup.compactor.errors", Map.of());
     }
-
-    /**
-     * Update the partitions of __consumer_offsets assigned to this processor.
-     */
-    public void updatePartitionAssigned(Set partitions) {
-        consumerOffsetsTopicPartitions.set(new HashSet<>(partitions));
-        lastCommittedOffsetState.invalidateUnassignedEntries(partitions, consumerOffsetsPartitionCount);
+    metricsProvider.recordTimer(
+        "ugroup.compactor.compact_duration", System.currentTimeMillis() - startTime, Map.of());
+  }
+
+  private void compactForGroupAndTopicsWatchList(long currentTime) {
+    Set consumerOffsetPartitions = consumerOffsetsTopicPartitions.get();
+    if (consumerOffsetPartitions == null || consumerOffsetPartitions.isEmpty()) {
+      return;
     }
 
-    private void compactCommittedOffsets() {
-        long startTime = System.currentTimeMillis();
-        try {
-            compactForGroupAndTopicsWatchList(startTime);
-        } catch (Exception e) {
-            logger.error("Error during offset compaction", e);
-            metricsProvider.incrementCounter("ugroup.compactor.errors", Map.of());
+    // Refresh and add watch list entries
+    for (WatchListProvider provider : watchListProviders) {
+      provider.refresh();
+      for (int partition : consumerOffsetPartitions) {
+        Set gats = provider.getGroupAndTopics().get(partition);
+        if (gats != null) {
+          addGroupAndTopicsToCompact(gats, currentTime);
         }
-        metricsProvider.recordTimer("ugroup.compactor.compact_duration",
-                System.currentTimeMillis() - startTime, Map.of());
+      }
     }
 
-    private void compactForGroupAndTopicsWatchList(long currentTime) {
-        Set consumerOffsetPartitions = consumerOffsetsTopicPartitions.get();
-        if (consumerOffsetPartitions == null || consumerOffsetPartitions.isEmpty()) {
-            return;
-        }
+    // Process items in pipeline
+    List itemsToRemove = new ArrayList<>();
 
-        // Refresh and add watch list entries
-        for (WatchListProvider provider : watchListProviders) {
-            provider.refresh();
-            for (int partition : consumerOffsetPartitions) {
-                Set gats = provider.getGroupAndTopics().get(partition);
-                if (gats != null) {
-                    addGroupAndTopicsToCompact(gats, currentTime);
-                }
-            }
-        }
+    for (Map.Entry entry : groupAndTopicsInPipeline.entrySet()) {
+      GroupAndTopic groupAndTopic = entry.getKey();
+      long timestamp = entry.getValue();
 
-        // Process items in pipeline
-        List itemsToRemove = new ArrayList<>();
-
-        for (Map.Entry entry : groupAndTopicsInPipeline.entrySet()) {
-            GroupAndTopic groupAndTopic = entry.getKey();
-            long timestamp = entry.getValue();
-
-            Map commitState =
-                    lastCommittedOffsetState.getGroupTopicLastCommittedOffsets(groupAndTopic);
-
-            // Check if we need to fetch offsets from broker
-            if (shouldFetchCommittedOffsets(commitState, timestamp, currentTime)) {
-                try {
-                    Map groupOffsets =
-                            offsetFetcher.listConsumerGroupOffsets(groupAndTopic.getGroup());
-
-                    for (Map.Entry offsetEntry : groupOffsets.entrySet()) {
-                        if (offsetEntry.getKey().topic().equals(groupAndTopic.getTopic())) {
-                            lastCommittedOffsetState.updateLastCommittedOffset(
-                                    groupAndTopic.getGroup(),
-                                    groupAndTopic.getTopic(),
-                                    offsetEntry.getKey().partition(),
-                                    offsetEntry.getValue().offset(),
-                                    currentTime);
-                        }
-                    }
-                    commitState = lastCommittedOffsetState.getGroupTopicLastCommittedOffsets(groupAndTopic);
-                } catch (Exception e) {
-                    logger.warn("Failed to fetch committed offsets for {}", groupAndTopic, e);
-                    metricsProvider.incrementCounter("ugroup.compactor.fetch_failed",
-                            Map.of("group", groupAndTopic.getGroup(), "topic", groupAndTopic.getTopic()));
-                }
-            }
+      Map commitState =
+          lastCommittedOffsetState.getGroupTopicLastCommittedOffsets(groupAndTopic);
 
-            // Check if ready to process
-            if (currentTime - timestamp >= compactingDelayMillis && !commitState.isEmpty()) {
-                Map finalCommitState = Map.copyOf(commitState);
-                executor.submit(() ->
-                        eventCallback.onCompactedOffsets(groupAndTopic, finalCommitState, System.currentTimeMillis()));
-                itemsToRemove.add(groupAndTopic);
+      // Check if we need to fetch offsets from broker
+      if (shouldFetchCommittedOffsets(commitState, timestamp, currentTime)) {
+        try {
+          Map groupOffsets =
+              offsetFetcher.listConsumerGroupOffsets(groupAndTopic.getGroup());
+
+          for (Map.Entry offsetEntry : groupOffsets.entrySet()) {
+            if (offsetEntry.getKey().topic().equals(groupAndTopic.getTopic())) {
+              lastCommittedOffsetState.updateLastCommittedOffset(
+                  groupAndTopic.getGroup(),
+                  groupAndTopic.getTopic(),
+                  offsetEntry.getKey().partition(),
+                  offsetEntry.getValue().offset(),
+                  currentTime);
             }
+          }
+          commitState = lastCommittedOffsetState.getGroupTopicLastCommittedOffsets(groupAndTopic);
+        } catch (Exception e) {
+          logger.warn("Failed to fetch committed offsets for {}", groupAndTopic, e);
+          metricsProvider.incrementCounter(
+              "ugroup.compactor.fetch_failed",
+              Map.of("group", groupAndTopic.getGroup(), "topic", groupAndTopic.getTopic()));
         }
-
-        itemsToRemove.forEach(groupAndTopicsInPipeline::remove);
+      }
+
+      // Check if ready to process
+      if (currentTime - timestamp >= compactingDelayMillis && !commitState.isEmpty()) {
+        Map finalCommitState = Map.copyOf(commitState);
+        executor.submit(
+            () ->
+                eventCallback.onCompactedOffsets(
+                    groupAndTopic, finalCommitState, System.currentTimeMillis()));
+        itemsToRemove.add(groupAndTopic);
+      }
     }
 
-    private boolean shouldFetchCommittedOffsets(
-            Map commitState,
-            long registrationTime,
-            long currentTime) {
-        // Fetch if no commits recorded or commits are stale
-        if (commitState.isEmpty()) {
-            return true;
-        }
-
-        // Check if any partition has stale commits (more than compacting delay)
-        long oldestCommit = commitState.values().stream()
-                .mapToLong(LastCommittedOffset::getCommitTimeMillis)
-                .min()
-                .orElse(0);
-
-        return currentTime - oldestCommit > compactingDelayMillis * 2;
-    }
+    itemsToRemove.forEach(groupAndTopicsInPipeline::remove);
+  }
 
-    @Override
-    public void close() {
-        scheduler.shutdown();
-        executor.shutdown();
-        try {
-            if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
-                scheduler.shutdownNow();
-            }
-            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
-                executor.shutdownNow();
-            }
-        } catch (InterruptedException e) {
-            scheduler.shutdownNow();
-            executor.shutdownNow();
-            Thread.currentThread().interrupt();
-        }
+  private boolean shouldFetchCommittedOffsets(
+      Map commitState, long registrationTime, long currentTime) {
+    // Fetch if no commits recorded or commits are stale
+    if (commitState.isEmpty()) {
+      return true;
     }
 
-    /**
-     * Callback interface for compacted offset events.
-     */
-    @FunctionalInterface
-    public interface CompactorEventCallback {
-        void onCompactedOffsets(GroupAndTopic groupAndTopic,
-                                Map lastCommittedOffsets,
-                                long enqueueTimestamp);
+    // Check if any partition has stale commits (more than compacting delay)
+    long oldestCommit =
+        commitState.values().stream()
+            .mapToLong(LastCommittedOffset::getCommitTimeMillis)
+            .min()
+            .orElse(0);
+
+    return currentTime - oldestCommit > compactingDelayMillis * 2;
+  }
+
+  @Override
+  public void close() {
+    scheduler.shutdown();
+    executor.shutdown();
+    try {
+      if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
+        scheduler.shutdownNow();
+      }
+      if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+        executor.shutdownNow();
+      }
+    } catch (InterruptedException e) {
+      scheduler.shutdownNow();
+      executor.shutdownNow();
+      Thread.currentThread().interrupt();
     }
+  }
+
+  /** Callback interface for compacted offset events. */
+  @FunctionalInterface
+  public interface CompactorEventCallback {
+    void onCompactedOffsets(
+        GroupAndTopic groupAndTopic,
+        Map lastCommittedOffsets,
+        long enqueueTimestamp);
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/processor/CompactedOffsetsProcessor.java b/src/main/java/com/uber/ugroup/processor/CompactedOffsetsProcessor.java
index 7abb438..7060045 100644
--- a/src/main/java/com/uber/ugroup/processor/CompactedOffsetsProcessor.java
+++ b/src/main/java/com/uber/ugroup/processor/CompactedOffsetsProcessor.java
@@ -17,21 +17,19 @@
 
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
-
 import java.util.Map;
 
-/**
- * Interface for processing compacted offset information for a consumer group-topic.
- */
+/** Interface for processing compacted offset information for a consumer group-topic. */
 @FunctionalInterface
 public interface CompactedOffsetsProcessor {
 
-    /**
-     * Process the compacted committed offsets for a group-topic combination.
-     *
-     * @param groupAndTopic        the consumer group and topic
-     * @param lastCommittedOffsets map of partition to last committed offset
-     * @return true if this processor handled the group-topic, false to try next processor
-     */
-    boolean process(GroupAndTopic groupAndTopic, Map lastCommittedOffsets);
+  /**
+   * Process the compacted committed offsets for a group-topic combination.
+   *
+   * @param groupAndTopic the consumer group and topic
+   * @param lastCommittedOffsets map of partition to last committed offset
+   * @return true if this processor handled the group-topic, false to try next processor
+   */
+  boolean process(
+      GroupAndTopic groupAndTopic, Map lastCommittedOffsets);
 }
diff --git a/src/main/java/com/uber/ugroup/processor/ConsumerOffsetTopicProcessor.java b/src/main/java/com/uber/ugroup/processor/ConsumerOffsetTopicProcessor.java
index 3c6a1cf..eabd86f 100644
--- a/src/main/java/com/uber/ugroup/processor/ConsumerOffsetTopicProcessor.java
+++ b/src/main/java/com/uber/ugroup/processor/ConsumerOffsetTopicProcessor.java
@@ -22,17 +22,6 @@
 import com.uber.ugroup.model.LastCommittedOffset;
 import com.uber.ugroup.watchlist.BlockList;
 import com.uber.ugroup.watchlist.WatchListProvider;
-import org.apache.kafka.common.protocol.ByteBufferAccessor;
-import org.apache.kafka.coordinator.group.generated.OffsetCommitKey;
-import org.apache.kafka.coordinator.group.generated.OffsetCommitValue;
-import org.apache.kafka.clients.consumer.ConsumerRecord;
-import org.apache.kafka.clients.consumer.ConsumerRecords;
-import org.apache.kafka.clients.consumer.KafkaConsumer;
-import org.apache.kafka.common.TopicPartition;
-import org.apache.kafka.common.internals.Topic;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.nio.ByteBuffer;
 import java.time.Duration;
 import java.util.HashSet;
@@ -44,6 +33,16 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.clients.consumer.ConsumerRecords;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.TopicPartition;
+import org.apache.kafka.common.internals.Topic;
+import org.apache.kafka.common.protocol.ByteBufferAccessor;
+import org.apache.kafka.coordinator.group.generated.OffsetCommitKey;
+import org.apache.kafka.coordinator.group.generated.OffsetCommitValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Consumes messages from the __consumer_offsets topic and extracts committed offset information.
@@ -51,231 +50,236 @@
  */
 public class ConsumerOffsetTopicProcessor implements Runnable, AutoCloseable {
 
-    private static final Logger logger = LoggerFactory.getLogger(ConsumerOffsetTopicProcessor.class);
-    private static final Duration POLL_TIMEOUT = Duration.ofMillis(100);
-    private static final long MAX_ENQUEUE_DELAY_MS = 5000L;
-
-    private final MetricsProvider metricsProvider;
-    private final UGroupProperties properties;
-    private final List compactedOffsetsProcessors;
-    private final KafkaConsumer consumer;
-    private final CommittedOffsetsCompactor compactor;
-    private final AtomicReference> assignedPartitions;
-    private final AtomicBoolean running;
-    private final CountDownLatch stoppedLatch;
-    private final OffsetFetcher offsetFetcher;
-    private final String consumerGroupId;
-
-    public ConsumerOffsetTopicProcessor(
-            MetricsProvider metricsProvider,
-            UGroupProperties properties,
-            KafkaConsumer consumer,
-            List compactedOffsetsProcessors,
-            OffsetFetcher offsetFetcher,
-            BlockList blockList,
-            List watchListProviders) {
-
-        this.metricsProvider = metricsProvider;
-        this.properties = properties;
-        this.consumer = consumer;
-        this.compactedOffsetsProcessors = compactedOffsetsProcessors;
-        this.offsetFetcher = offsetFetcher;
-        this.consumerGroupId = properties.getKafka().getConsumerGroup();
-
-        this.compactor = new CommittedOffsetsCompactor(
-                metricsProvider,
-                offsetFetcher,
-                blockList,
-                watchListProviders,
-                this::processCompactedOffset,
-                properties.getProcessing().getParallelism(),
-                properties.getProcessing().getConsumerOffsetsPartitionCount(),
-                properties.getProcessing().getCompactionIntervalMs(),
-                properties.getProcessing().getLagReportIntervalMs());
-
-        this.assignedPartitions = new AtomicReference<>(new HashSet<>());
-        this.running = new AtomicBoolean(true);
-        this.stoppedLatch = new CountDownLatch(1);
+  private static final Logger logger = LoggerFactory.getLogger(ConsumerOffsetTopicProcessor.class);
+  private static final Duration POLL_TIMEOUT = Duration.ofMillis(100);
+  private static final long MAX_ENQUEUE_DELAY_MS = 5000L;
+
+  private final MetricsProvider metricsProvider;
+  private final UGroupProperties properties;
+  private final List compactedOffsetsProcessors;
+  private final KafkaConsumer consumer;
+  private final CommittedOffsetsCompactor compactor;
+  private final AtomicReference> assignedPartitions;
+  private final AtomicBoolean running;
+  private final CountDownLatch stoppedLatch;
+  private final OffsetFetcher offsetFetcher;
+  private final String consumerGroupId;
+
+  public ConsumerOffsetTopicProcessor(
+      MetricsProvider metricsProvider,
+      UGroupProperties properties,
+      KafkaConsumer consumer,
+      List compactedOffsetsProcessors,
+      OffsetFetcher offsetFetcher,
+      BlockList blockList,
+      List watchListProviders) {
+
+    this.metricsProvider = metricsProvider;
+    this.properties = properties;
+    this.consumer = consumer;
+    this.compactedOffsetsProcessors = compactedOffsetsProcessors;
+    this.offsetFetcher = offsetFetcher;
+    this.consumerGroupId = properties.getKafka().getConsumerGroup();
+
+    this.compactor =
+        new CommittedOffsetsCompactor(
+            metricsProvider,
+            offsetFetcher,
+            blockList,
+            watchListProviders,
+            this::processCompactedOffset,
+            properties.getProcessing().getParallelism(),
+            properties.getProcessing().getConsumerOffsetsPartitionCount(),
+            properties.getProcessing().getCompactionIntervalMs(),
+            properties.getProcessing().getLagReportIntervalMs());
+
+    this.assignedPartitions = new AtomicReference<>(new HashSet<>());
+    this.running = new AtomicBoolean(true);
+    this.stoppedLatch = new CountDownLatch(1);
+  }
+
+  /** Process compacted offsets through the processor chain. */
+  void processCompactedOffset(
+      GroupAndTopic groupAndTopic,
+      Map lastCommittedOffsets,
+      long enqueueTimestamp) {
+    long enqueueDelay = System.currentTimeMillis() - enqueueTimestamp;
+    metricsProvider.recordGauge(
+        "ugroup.processor.enqueue_delay",
+        enqueueDelay,
+        Map.of("group", groupAndTopic.getGroup(), "topic", groupAndTopic.getTopic()));
+
+    if (enqueueDelay > MAX_ENQUEUE_DELAY_MS) {
+      metricsProvider.incrementCounter("ugroup.processor.skipped_delayed", Map.of());
+      return;
     }
 
-    /**
-     * Process compacted offsets through the processor chain.
-     */
-    void processCompactedOffset(GroupAndTopic groupAndTopic,
-                                Map lastCommittedOffsets,
-                                long enqueueTimestamp) {
-        long enqueueDelay = System.currentTimeMillis() - enqueueTimestamp;
-        metricsProvider.recordGauge("ugroup.processor.enqueue_delay", enqueueDelay,
-                Map.of("group", groupAndTopic.getGroup(), "topic", groupAndTopic.getTopic()));
-
-        if (enqueueDelay > MAX_ENQUEUE_DELAY_MS) {
-            metricsProvider.incrementCounter("ugroup.processor.skipped_delayed", Map.of());
-            return;
-        }
-
-        for (CompactedOffsetsProcessor processor : compactedOffsetsProcessors) {
-            if (processor.process(groupAndTopic, lastCommittedOffsets)) {
-                break;
+    for (CompactedOffsetsProcessor processor : compactedOffsetsProcessors) {
+      if (processor.process(groupAndTopic, lastCommittedOffsets)) {
+        break;
+      }
+    }
+  }
+
+  /**
+   * Offset commit record type versions in __consumer_offsets key. Version 0/1 = offset commit,
+   * version 2 = group metadata.
+   */
+  private static final short OFFSET_COMMIT_KEY_VERSION = 1;
+
+  private static final short LEGACY_OFFSET_COMMIT_KEY_VERSION = 0;
+
+  /** Process a batch of records from __consumer_offsets. */
+  void processRecords(ConsumerRecords records, long reportTime) {
+    Set groupAndTopicsSeen = new HashSet<>();
+
+    for (ConsumerRecord record : records) {
+      if (record.key() == null) {
+        continue;
+      }
+
+      try {
+        ByteBuffer keyBuffer = ByteBuffer.wrap(record.key());
+        short version = keyBuffer.getShort();
+
+        if (version == OFFSET_COMMIT_KEY_VERSION || version == LEGACY_OFFSET_COMMIT_KEY_VERSION) {
+          OffsetCommitKey offsetKey =
+              new OffsetCommitKey(new ByteBufferAccessor(keyBuffer), (short) 0);
+
+          if (record.value() != null) {
+            metricsProvider.incrementCounter("ugroup.messages.processed", Map.of());
+
+            ByteBuffer valueBuffer = ByteBuffer.wrap(record.value());
+            short valueVersion = valueBuffer.getShort();
+            OffsetCommitValue offsetValue =
+                new OffsetCommitValue(new ByteBufferAccessor(valueBuffer), valueVersion);
+
+            compactor.offsetCommitted(
+                offsetKey.group(), offsetKey.topic(), offsetKey.partition(), offsetValue.offset());
+
+            GroupAndTopic key = new GroupAndTopic(offsetKey.group(), offsetKey.topic());
+            groupAndTopicsSeen.add(key);
+
+            if (System.currentTimeMillis() >= reportTime) {
+              metricsProvider.incrementCounter(
+                  "ugroup.offset_commits",
+                  Map.of(
+                      "topic",
+                      key.getTopic(),
+                      "group",
+                      key.getGroup(),
+                      "partition",
+                      String.valueOf(offsetKey.partition())));
             }
+          } else {
+            metricsProvider.incrementCounter(
+                "ugroup.messages.skipped", Map.of("reason", "null_value"));
+          }
+        } else {
+          metricsProvider.incrementCounter(
+              "ugroup.messages.skipped", Map.of("reason", "not_offset_key"));
         }
+      } catch (Exception e) {
+        logger.debug("Error parsing consumer offset record", e);
+        metricsProvider.incrementCounter("ugroup.messages.errors", Map.of());
+      }
     }
 
-    /**
-     * Offset commit record type versions in __consumer_offsets key.
-     * Version 0/1 = offset commit, version 2 = group metadata.
-     */
-    private static final short OFFSET_COMMIT_KEY_VERSION = 1;
-    private static final short LEGACY_OFFSET_COMMIT_KEY_VERSION = 0;
-
-    /**
-     * Process a batch of records from __consumer_offsets.
-     */
-    void processRecords(ConsumerRecords records, long reportTime) {
-        Set groupAndTopicsSeen = new HashSet<>();
-
-        for (ConsumerRecord record : records) {
-            if (record.key() == null) {
-                continue;
-            }
+    compactor.addGroupAndTopicsToCompact(groupAndTopicsSeen, System.currentTimeMillis());
+  }
 
-            try {
-                ByteBuffer keyBuffer = ByteBuffer.wrap(record.key());
-                short version = keyBuffer.getShort();
-
-                if (version == OFFSET_COMMIT_KEY_VERSION || version == LEGACY_OFFSET_COMMIT_KEY_VERSION) {
-                    OffsetCommitKey offsetKey = new OffsetCommitKey(
-                            new ByteBufferAccessor(keyBuffer), (short) 0);
-
-                    if (record.value() != null) {
-                        metricsProvider.incrementCounter("ugroup.messages.processed", Map.of());
-
-                        ByteBuffer valueBuffer = ByteBuffer.wrap(record.value());
-                        short valueVersion = valueBuffer.getShort();
-                        OffsetCommitValue offsetValue = new OffsetCommitValue(
-                                new ByteBufferAccessor(valueBuffer), valueVersion);
-
-                        compactor.offsetCommitted(
-                                offsetKey.group(),
-                                offsetKey.topic(),
-                                offsetKey.partition(),
-                                offsetValue.offset());
-
-                        GroupAndTopic key = new GroupAndTopic(offsetKey.group(), offsetKey.topic());
-                        groupAndTopicsSeen.add(key);
-
-                        if (System.currentTimeMillis() >= reportTime) {
-                            metricsProvider.incrementCounter("ugroup.offset_commits",
-                                    Map.of("topic", key.getTopic(),
-                                            "group", key.getGroup(),
-                                            "partition", String.valueOf(offsetKey.partition())));
-                        }
-                    } else {
-                        metricsProvider.incrementCounter("ugroup.messages.skipped", Map.of("reason", "null_value"));
-                    }
-                } else {
-                    metricsProvider.incrementCounter("ugroup.messages.skipped", Map.of("reason", "not_offset_key"));
-                }
-            } catch (Exception e) {
-                logger.debug("Error parsing consumer offset record", e);
-                metricsProvider.incrementCounter("ugroup.messages.errors", Map.of());
-            }
-        }
+  @Override
+  public void run() {
+    logger.info("Starting ConsumerOffsetTopicProcessor");
+    long reportTime =
+        System.currentTimeMillis() + properties.getProcessing().getLagReportIntervalMs();
 
-        compactor.addGroupAndTopicsToCompact(groupAndTopicsSeen, System.currentTimeMillis());
+    try {
+      while (running.get()) {
+        try {
+          reportTime = reportConsumerOffsetConsumerLag(reportTime);
+          processingLoop(reportTime);
+        } catch (Exception e) {
+          if (!running.get()) {
+            break;
+          }
+          logger.error("Error in processing loop", e);
+          metricsProvider.incrementCounter("ugroup.processor.errors", Map.of());
+        }
+      }
+    } finally {
+      stoppedLatch.countDown();
+      logger.info("ConsumerOffsetTopicProcessor stopped");
     }
+  }
 
-    @Override
-    public void run() {
-        logger.info("Starting ConsumerOffsetTopicProcessor");
-        long reportTime = System.currentTimeMillis() + properties.getProcessing().getLagReportIntervalMs();
+  void processingLoop(long reportTime) {
+    ConsumerRecords records = consumer.poll(POLL_TIMEOUT);
+    if (records != null && !records.isEmpty()) {
+      metricsProvider.incrementCounter("ugroup.messages.read", records.count(), Map.of());
+      processRecords(records, reportTime);
+    }
 
-        try {
-            while (running.get()) {
-                try {
-                    reportTime = reportConsumerOffsetConsumerLag(reportTime);
-                    processingLoop(reportTime);
-                } catch (Exception e) {
-                    if (!running.get()) {
-                        break;
-                    }
-                    logger.error("Error in processing loop", e);
-                    metricsProvider.incrementCounter("ugroup.processor.errors", Map.of());
-                }
-            }
-        } finally {
-            stoppedLatch.countDown();
-            logger.info("ConsumerOffsetTopicProcessor stopped");
-        }
+    // Track assigned partitions
+    Set topicPartitions = consumer.assignment();
+    Set partitions =
+        topicPartitions.stream()
+            .filter(tp -> Topic.GROUP_METADATA_TOPIC_NAME.equals(tp.topic()))
+            .map(TopicPartition::partition)
+            .collect(Collectors.toSet());
+
+    Set currentAssigned = assignedPartitions.get();
+    if (!currentAssigned.equals(partitions)) {
+      logger.info("Partition assignment changed: {} -> {}", currentAssigned, partitions);
+      metricsProvider.recordGauge("ugroup.assigned_partitions", partitions.size(), Map.of());
     }
 
-    void processingLoop(long reportTime) {
-        ConsumerRecords records = consumer.poll(POLL_TIMEOUT);
-        if (records != null && !records.isEmpty()) {
-            metricsProvider.incrementCounter("ugroup.messages.read", records.count(), Map.of());
-            processRecords(records, reportTime);
-        }
+    assignedPartitions.set(partitions);
+    compactor.updatePartitionAssigned(partitions);
+  }
 
-        // Track assigned partitions
+  /** Report lag for this processor's own consumption of __consumer_offsets. */
+  long reportConsumerOffsetConsumerLag(long reportTime) {
+    if (System.currentTimeMillis() >= reportTime) {
+      try {
         Set topicPartitions = consumer.assignment();
-        Set partitions = topicPartitions.stream()
-                .filter(tp -> Topic.GROUP_METADATA_TOPIC_NAME.equals(tp.topic()))
-                .map(TopicPartition::partition)
-                .collect(Collectors.toSet());
-
-        Set currentAssigned = assignedPartitions.get();
-        if (!currentAssigned.equals(partitions)) {
-            logger.info("Partition assignment changed: {} -> {}", currentAssigned, partitions);
-            metricsProvider.recordGauge("ugroup.assigned_partitions", partitions.size(), Map.of());
-        }
+        Map endOffsets = consumer.endOffsets(topicPartitions);
 
-        assignedPartitions.set(partitions);
-        compactor.updatePartitionAssigned(partitions);
-    }
-
-    /**
-     * Report lag for this processor's own consumption of __consumer_offsets.
-     */
-    long reportConsumerOffsetConsumerLag(long reportTime) {
-        if (System.currentTimeMillis() >= reportTime) {
-            try {
-                Set topicPartitions = consumer.assignment();
-                Map endOffsets = consumer.endOffsets(topicPartitions);
-
-                for (TopicPartition tp : topicPartitions) {
-                    long currentPosition = consumer.position(tp);
-                    long endOffset = endOffsets.getOrDefault(tp, 0L);
-                    long lag = Math.max(0, endOffset - currentPosition);
-
-                    metricsProvider.recordGauge("ugroup.self_lag", lag,
-                            Map.of("partition", String.valueOf(tp.partition())));
-                }
-            } catch (Exception e) {
-                logger.warn("Error reporting self lag", e);
-            }
+        for (TopicPartition tp : topicPartitions) {
+          long currentPosition = consumer.position(tp);
+          long endOffset = endOffsets.getOrDefault(tp, 0L);
+          long lag = Math.max(0, endOffset - currentPosition);
 
-            return System.currentTimeMillis() + properties.getProcessing().getLagReportIntervalMs();
+          metricsProvider.recordGauge(
+              "ugroup.self_lag", lag, Map.of("partition", String.valueOf(tp.partition())));
         }
-        return reportTime;
-    }
+      } catch (Exception e) {
+        logger.warn("Error reporting self lag", e);
+      }
 
-    public void stop() {
-        running.set(false);
+      return System.currentTimeMillis() + properties.getProcessing().getLagReportIntervalMs();
     }
-
-    @Override
-    public void close() {
-        stop();
-        try {
-            if (!stoppedLatch.await(5, TimeUnit.SECONDS)) {
-                logger.warn("Timed out waiting for processor to stop");
-            }
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-        }
-        try {
-            compactor.close();
-        } catch (Exception e) {
-            logger.warn("Error closing compactor", e);
-        }
+    return reportTime;
+  }
+
+  public void stop() {
+    running.set(false);
+  }
+
+  @Override
+  public void close() {
+    stop();
+    try {
+      if (!stoppedLatch.await(5, TimeUnit.SECONDS)) {
+        logger.warn("Timed out waiting for processor to stop");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+    }
+    try {
+      compactor.close();
+    } catch (Exception e) {
+      logger.warn("Error closing compactor", e);
     }
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/processor/LagCalculationUtils.java b/src/main/java/com/uber/ugroup/processor/LagCalculationUtils.java
index ce8f3aa..f6a5bb7 100644
--- a/src/main/java/com/uber/ugroup/processor/LagCalculationUtils.java
+++ b/src/main/java/com/uber/ugroup/processor/LagCalculationUtils.java
@@ -15,61 +15,59 @@
  */
 package com.uber.ugroup.processor;
 
-/**
- * Utility class for calculating consumer lag.
- */
+/** Utility class for calculating consumer lag. */
 public final class LagCalculationUtils {
 
-    private LagCalculationUtils() {
-    }
+  private LagCalculationUtils() {}
 
-    /**
-     * Calculate consumer offset lag.
-     *
-     * @param beginningOffset the beginning offset (low watermark)
-     * @param committedOffset the committed offset
-     * @param endOffset       the end offset (high watermark)
-     * @return the number of messages behind (lag)
-     */
-    public static long getConsumerLag(long beginningOffset, long committedOffset, long endOffset) {
-        if (committedOffset < 0) {
-            // Never committed, lag is the entire topic
-            return endOffset - beginningOffset;
-        }
-        if (committedOffset < beginningOffset) {
-            // Committed offset is before log start (data deleted), lag is entire available range
-            return endOffset - beginningOffset;
-        }
-        return Math.max(0, endOffset - committedOffset);
+  /**
+   * Calculate consumer offset lag.
+   *
+   * @param beginningOffset the beginning offset (low watermark)
+   * @param committedOffset the committed offset
+   * @param endOffset the end offset (high watermark)
+   * @return the number of messages behind (lag)
+   */
+  public static long getConsumerLag(long beginningOffset, long committedOffset, long endOffset) {
+    if (committedOffset < 0) {
+      // Never committed, lag is the entire topic
+      return endOffset - beginningOffset;
+    }
+    if (committedOffset < beginningOffset) {
+      // Committed offset is before log start (data deleted), lag is entire available range
+      return endOffset - beginningOffset;
     }
+    return Math.max(0, endOffset - committedOffset);
+  }
 
-    /**
-     * Calculate consumer lag as a percentage.
-     *
-     * @param beginningOffset the beginning offset
-     * @param committedOffset the committed offset
-     * @param endOffset       the end offset
-     * @return percentage of messages remaining (0-100)
-     */
-    public static double getConsumerLagPercentage(long beginningOffset, long committedOffset, long endOffset) {
-        long totalMessages = endOffset - beginningOffset;
-        if (totalMessages <= 0) {
-            return 0.0;
-        }
-        long lag = getConsumerLag(beginningOffset, committedOffset, endOffset);
-        return (lag * 100.0) / totalMessages;
+  /**
+   * Calculate consumer lag as a percentage.
+   *
+   * @param beginningOffset the beginning offset
+   * @param committedOffset the committed offset
+   * @param endOffset the end offset
+   * @return percentage of messages remaining (0-100)
+   */
+  public static double getConsumerLagPercentage(
+      long beginningOffset, long committedOffset, long endOffset) {
+    long totalMessages = endOffset - beginningOffset;
+    if (totalMessages <= 0) {
+      return 0.0;
     }
+    long lag = getConsumerLag(beginningOffset, committedOffset, endOffset);
+    return (lag * 100.0) / totalMessages;
+  }
 
-    /**
-     * Get time elapsed since last commit.
-     *
-     * @param lastCommitTimeMillis the timestamp of the last commit
-     * @return milliseconds since last commit
-     */
-    public static long getTimeSinceLastCommit(long lastCommitTimeMillis) {
-        if (lastCommitTimeMillis <= 0) {
-            return Long.MAX_VALUE;
-        }
-        return System.currentTimeMillis() - lastCommitTimeMillis;
+  /**
+   * Get time elapsed since last commit.
+   *
+   * @param lastCommitTimeMillis the timestamp of the last commit
+   * @return milliseconds since last commit
+   */
+  public static long getTimeSinceLastCommit(long lastCommitTimeMillis) {
+    if (lastCommitTimeMillis <= 0) {
+      return Long.MAX_VALUE;
     }
+    return System.currentTimeMillis() - lastCommitTimeMillis;
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/processor/state/LastCommittedOffsetState.java b/src/main/java/com/uber/ugroup/processor/state/LastCommittedOffsetState.java
index 03699b3..3d6458e 100644
--- a/src/main/java/com/uber/ugroup/processor/state/LastCommittedOffsetState.java
+++ b/src/main/java/com/uber/ugroup/processor/state/LastCommittedOffsetState.java
@@ -18,67 +18,59 @@
 import com.uber.ugroup.metrics.MetricsProvider;
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
-
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
-/**
- * Tracks the last committed offsets for all consumer group-topic-partition combinations.
- */
+/** Tracks the last committed offsets for all consumer group-topic-partition combinations. */
 public class LastCommittedOffsetState {
 
-    private final ConcurrentHashMap> state;
-    private final MetricsProvider metricsProvider;
+  private final ConcurrentHashMap>
+      state;
+  private final MetricsProvider metricsProvider;
 
-    public LastCommittedOffsetState(MetricsProvider metricsProvider) {
-        this.state = new ConcurrentHashMap<>();
-        this.metricsProvider = metricsProvider;
+  public LastCommittedOffsetState(MetricsProvider metricsProvider) {
+    this.state = new ConcurrentHashMap<>();
+    this.metricsProvider = metricsProvider;
 
-        // Register gauge for cache size
-        metricsProvider.registerGauge("ugroup.offset_state.size", state::size, Map.of());
-    }
+    // Register gauge for cache size
+    metricsProvider.registerGauge("ugroup.offset_state.size", state::size, Map.of());
+  }
 
-    /**
-     * Update the last committed offset for a group-topic-partition.
-     */
-    public void updateLastCommittedOffset(String group, String topic, int partition,
-                                          long offset, long commitTimeMillis) {
-        GroupAndTopic key = new GroupAndTopic(group, topic);
-        state.computeIfAbsent(key, k -> new ConcurrentHashMap<>())
-                .put(partition, new LastCommittedOffset(offset, commitTimeMillis));
-    }
+  /** Update the last committed offset for a group-topic-partition. */
+  public void updateLastCommittedOffset(
+      String group, String topic, int partition, long offset, long commitTimeMillis) {
+    GroupAndTopic key = new GroupAndTopic(group, topic);
+    state
+        .computeIfAbsent(key, k -> new ConcurrentHashMap<>())
+        .put(partition, new LastCommittedOffset(offset, commitTimeMillis));
+  }
 
-    /**
-     * Get the last committed offsets for a group-topic combination.
-     */
-    public Map getGroupTopicLastCommittedOffsets(GroupAndTopic groupAndTopic) {
-        ConcurrentHashMap offsets = state.get(groupAndTopic);
-        return offsets != null ? Map.copyOf(offsets) : Map.of();
-    }
+  /** Get the last committed offsets for a group-topic combination. */
+  public Map getGroupTopicLastCommittedOffsets(
+      GroupAndTopic groupAndTopic) {
+    ConcurrentHashMap offsets = state.get(groupAndTopic);
+    return offsets != null ? Map.copyOf(offsets) : Map.of();
+  }
 
-    /**
-     * Invalidate entries for partitions of __consumer_offsets that are no longer assigned.
-     */
-    public void invalidateUnassignedEntries(Set assignedPartitions, int totalPartitions) {
-        state.keySet().removeIf(gat -> {
-            int partition = GroupAndTopic.calculatePartition(gat.getGroup(), totalPartitions);
-            return !assignedPartitions.contains(partition);
-        });
-    }
+  /** Invalidate entries for partitions of __consumer_offsets that are no longer assigned. */
+  public void invalidateUnassignedEntries(Set assignedPartitions, int totalPartitions) {
+    state
+        .keySet()
+        .removeIf(
+            gat -> {
+              int partition = GroupAndTopic.calculatePartition(gat.getGroup(), totalPartitions);
+              return !assignedPartitions.contains(partition);
+            });
+  }
 
-    /**
-     * Get the number of tracked group-topic combinations.
-     */
-    public int size() {
-        return state.size();
-    }
+  /** Get the number of tracked group-topic combinations. */
+  public int size() {
+    return state.size();
+  }
 
-    /**
-     * Clear all state.
-     */
-    public void clear() {
-        state.clear();
-    }
+  /** Clear all state. */
+  public void clear() {
+    state.clear();
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/watchlist/AllConsumersWatchListProvider.java b/src/main/java/com/uber/ugroup/watchlist/AllConsumersWatchListProvider.java
index 4faa620..d2e3ab4 100644
--- a/src/main/java/com/uber/ugroup/watchlist/AllConsumersWatchListProvider.java
+++ b/src/main/java/com/uber/ugroup/watchlist/AllConsumersWatchListProvider.java
@@ -17,57 +17,55 @@
 
 import com.uber.ugroup.model.ConsumerMetadata;
 import com.uber.ugroup.model.GroupAndTopic;
-
 import java.util.Collections;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
 /**
- * WatchListProvider that monitors ALL consumer groups.
- * This is the default provider when no specific filtering is configured.
+ * WatchListProvider that monitors ALL consumer groups. This is the default provider when no
+ * specific filtering is configured.
  */
 public class AllConsumersWatchListProvider implements WatchListProvider {
 
-    private static final ConsumerMetadata DEFAULT_METADATA = ConsumerMetadata.builder()
-            .consumerType("default")
-            .build();
+  private static final ConsumerMetadata DEFAULT_METADATA =
+      ConsumerMetadata.builder().consumerType("default").build();
 
-    @Override
-    public String getName() {
-        return "all";
-    }
+  @Override
+  public String getName() {
+    return "all";
+  }
 
-    @Override
-    public Map> getGroupAndTopics() {
-        // Returns empty - this provider accepts everything dynamically
-        return Collections.emptyMap();
-    }
+  @Override
+  public Map> getGroupAndTopics() {
+    // Returns empty - this provider accepts everything dynamically
+    return Collections.emptyMap();
+  }
 
-    @Override
-    public Optional getMetadata(GroupAndTopic groupAndTopic) {
-        return Optional.of(DEFAULT_METADATA);
-    }
+  @Override
+  public Optional getMetadata(GroupAndTopic groupAndTopic) {
+    return Optional.of(DEFAULT_METADATA);
+  }
 
-    @Override
-    public Map> getGroupAndTopicsMetadata() {
-        return Collections.emptyMap();
-    }
+  @Override
+  public Map> getGroupAndTopicsMetadata() {
+    return Collections.emptyMap();
+  }
 
-    @Override
-    public void refresh() {
-        // No-op - this provider doesn't have a backing store
-    }
+  @Override
+  public void refresh() {
+    // No-op - this provider doesn't have a backing store
+  }
 
-    @Override
-    public boolean contains(GroupAndTopic groupAndTopic) {
-        // Accept everything
-        return true;
-    }
+  @Override
+  public boolean contains(GroupAndTopic groupAndTopic) {
+    // Accept everything
+    return true;
+  }
 
-    @Override
-    public int getPriority() {
-        // Lowest priority - only used when no other provider matches
-        return Integer.MAX_VALUE;
-    }
+  @Override
+  public int getPriority() {
+    // Lowest priority - only used when no other provider matches
+    return Integer.MAX_VALUE;
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/watchlist/BlockList.java b/src/main/java/com/uber/ugroup/watchlist/BlockList.java
index f084729..e5f23bf 100644
--- a/src/main/java/com/uber/ugroup/watchlist/BlockList.java
+++ b/src/main/java/com/uber/ugroup/watchlist/BlockList.java
@@ -16,12 +16,6 @@
 package com.uber.ugroup.watchlist;
 
 import com.uber.ugroup.model.GroupAndTopic;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.yaml.snakeyaml.LoaderOptions;
-import org.yaml.snakeyaml.Yaml;
-import org.yaml.snakeyaml.constructor.SafeConstructor;
-
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -32,6 +26,11 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
 
 /**
  * Maintains a block list of consumer groups/topics that should be excluded from monitoring.
@@ -39,153 +38,144 @@
  */
 public class BlockList {
 
-    private static final Logger logger = LoggerFactory.getLogger(BlockList.class);
-
-    private volatile Set blockedGroups;
-    private volatile Set blockedGroupAndTopics;
-    private volatile List blockedGroupPatterns;
-
-    public BlockList() {
-        this.blockedGroups = Collections.emptySet();
-        this.blockedGroupAndTopics = Collections.emptySet();
-        this.blockedGroupPatterns = Collections.emptyList();
+  private static final Logger logger = LoggerFactory.getLogger(BlockList.class);
+
+  private volatile Set blockedGroups;
+  private volatile Set blockedGroupAndTopics;
+  private volatile List blockedGroupPatterns;
+
+  public BlockList() {
+    this.blockedGroups = Collections.emptySet();
+    this.blockedGroupAndTopics = Collections.emptySet();
+    this.blockedGroupPatterns = Collections.emptyList();
+  }
+
+  /**
+   * Create a BlockList from a YAML file.
+   *
+   * 

Expected format: + * + *

+   * blocklist:
+   *   groups:
+   *     - blocked-group-1
+   *     - blocked-group-2
+   *   group-patterns:
+   *     - "test-.*"
+   *     - ".*-staging"
+   *   group-topics:
+   *     - group: some-group
+   *       topic: some-topic
+   * 
+ */ + @SuppressWarnings("unchecked") + public static BlockList fromYaml(String yamlFilePath) { + BlockList blockList = new BlockList(); + + if (yamlFilePath == null || yamlFilePath.isEmpty()) { + return blockList; } - /** - * Create a BlockList from a YAML file. - * - * Expected format: - *
-     * blocklist:
-     *   groups:
-     *     - blocked-group-1
-     *     - blocked-group-2
-     *   group-patterns:
-     *     - "test-.*"
-     *     - ".*-staging"
-     *   group-topics:
-     *     - group: some-group
-     *       topic: some-topic
-     * 
- */ - @SuppressWarnings("unchecked") - public static BlockList fromYaml(String yamlFilePath) { - BlockList blockList = new BlockList(); - - if (yamlFilePath == null || yamlFilePath.isEmpty()) { - return blockList; + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + try (InputStream input = new FileInputStream(yamlFilePath)) { + Map root = yaml.load(input); + Map blocklist = (Map) root.get("blocklist"); + + if (blocklist != null) { + // Load blocked groups + List groups = + (List) blocklist.getOrDefault("groups", Collections.emptyList()); + blockList.blockedGroups = ConcurrentHashMap.newKeySet(); + blockList.blockedGroups.addAll(groups); + + // Load group patterns + List patterns = + (List) blocklist.getOrDefault("group-patterns", Collections.emptyList()); + blockList.blockedGroupPatterns = patterns.stream().map(Pattern::compile).toList(); + + // Load group-topic pairs + List> groupTopics = + (List>) + blocklist.getOrDefault("group-topics", Collections.emptyList()); + blockList.blockedGroupAndTopics = new HashSet<>(); + for (Map gt : groupTopics) { + blockList.blockedGroupAndTopics.add(new GroupAndTopic(gt.get("group"), gt.get("topic"))); } - Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); - try (InputStream input = new FileInputStream(yamlFilePath)) { - Map root = yaml.load(input); - Map blocklist = (Map) root.get("blocklist"); - - if (blocklist != null) { - // Load blocked groups - List groups = (List) blocklist.getOrDefault("groups", Collections.emptyList()); - blockList.blockedGroups = ConcurrentHashMap.newKeySet(); - blockList.blockedGroups.addAll(groups); - - // Load group patterns - List patterns = (List) blocklist.getOrDefault("group-patterns", Collections.emptyList()); - blockList.blockedGroupPatterns = patterns.stream() - .map(Pattern::compile) - .toList(); - - // Load group-topic pairs - List> groupTopics = (List>) - blocklist.getOrDefault("group-topics", Collections.emptyList()); - blockList.blockedGroupAndTopics = new HashSet<>(); - for (Map gt : groupTopics) { - blockList.blockedGroupAndTopics.add(new GroupAndTopic(gt.get("group"), gt.get("topic"))); - } - - logger.info("Loaded blocklist: {} groups, {} patterns, {} group-topics", - blockList.blockedGroups.size(), - blockList.blockedGroupPatterns.size(), - blockList.blockedGroupAndTopics.size()); - } - - } catch (IOException e) { - logger.error("Failed to load blocklist from {}", yamlFilePath, e); - } + logger.info( + "Loaded blocklist: {} groups, {} patterns, {} group-topics", + blockList.blockedGroups.size(), + blockList.blockedGroupPatterns.size(), + blockList.blockedGroupAndTopics.size()); + } - return blockList; + } catch (IOException e) { + logger.error("Failed to load blocklist from {}", yamlFilePath, e); } - /** - * Check if a group-topic combination is blocked. - */ - public boolean isBlocked(GroupAndTopic groupAndTopic) { - // Check exact group-topic match - if (blockedGroupAndTopics.contains(groupAndTopic)) { - return true; - } - - // Check group match - if (blockedGroups.contains(groupAndTopic.getGroup())) { - return true; - } - - // Check patterns - for (Pattern pattern : blockedGroupPatterns) { - if (pattern.matcher(groupAndTopic.getGroup()).matches()) { - return true; - } - } + return blockList; + } - return false; + /** Check if a group-topic combination is blocked. */ + public boolean isBlocked(GroupAndTopic groupAndTopic) { + // Check exact group-topic match + if (blockedGroupAndTopics.contains(groupAndTopic)) { + return true; } - /** - * Check if a group is blocked (regardless of topic). - */ - public boolean isGroupBlocked(String groupId) { - if (blockedGroups.contains(groupId)) { - return true; - } - - for (Pattern pattern : blockedGroupPatterns) { - if (pattern.matcher(groupId).matches()) { - return true; - } - } - - return false; + // Check group match + if (blockedGroups.contains(groupAndTopic.getGroup())) { + return true; } - /** - * Add a group to the blocklist at runtime. - */ - public void blockGroup(String groupId) { - Set newBlocked = ConcurrentHashMap.newKeySet(); - newBlocked.addAll(blockedGroups); - newBlocked.add(groupId); - blockedGroups = newBlocked; + // Check patterns + for (Pattern pattern : blockedGroupPatterns) { + if (pattern.matcher(groupAndTopic.getGroup()).matches()) { + return true; + } } - /** - * Remove a group from the blocklist. - */ - public void unblockGroup(String groupId) { - Set newBlocked = ConcurrentHashMap.newKeySet(); - newBlocked.addAll(blockedGroups); - newBlocked.remove(groupId); - blockedGroups = newBlocked; - } + return false; + } - /** - * Get the set of explicitly blocked groups. - */ - public Set getBlockedGroups() { - return Collections.unmodifiableSet(blockedGroups); + /** Check if a group is blocked (regardless of topic). */ + public boolean isGroupBlocked(String groupId) { + if (blockedGroups.contains(groupId)) { + return true; } - /** - * Returns an empty blocklist. - */ - public static BlockList empty() { - return new BlockList(); + for (Pattern pattern : blockedGroupPatterns) { + if (pattern.matcher(groupId).matches()) { + return true; + } } + + return false; + } + + /** Add a group to the blocklist at runtime. */ + public void blockGroup(String groupId) { + Set newBlocked = ConcurrentHashMap.newKeySet(); + newBlocked.addAll(blockedGroups); + newBlocked.add(groupId); + blockedGroups = newBlocked; + } + + /** Remove a group from the blocklist. */ + public void unblockGroup(String groupId) { + Set newBlocked = ConcurrentHashMap.newKeySet(); + newBlocked.addAll(blockedGroups); + newBlocked.remove(groupId); + blockedGroups = newBlocked; + } + + /** Get the set of explicitly blocked groups. */ + public Set getBlockedGroups() { + return Collections.unmodifiableSet(blockedGroups); + } + + /** Returns an empty blocklist. */ + public static BlockList empty() { + return new BlockList(); + } } diff --git a/src/main/java/com/uber/ugroup/watchlist/RegexWatchListProvider.java b/src/main/java/com/uber/ugroup/watchlist/RegexWatchListProvider.java index d50467b..d087dae 100644 --- a/src/main/java/com/uber/ugroup/watchlist/RegexWatchListProvider.java +++ b/src/main/java/com/uber/ugroup/watchlist/RegexWatchListProvider.java @@ -17,9 +17,6 @@ import com.uber.ugroup.model.ConsumerMetadata; import com.uber.ugroup.model.GroupAndTopic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -27,101 +24,101 @@ import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -/** - * WatchListProvider that filters consumer groups based on regex patterns. - */ +/** WatchListProvider that filters consumer groups based on regex patterns. */ public class RegexWatchListProvider implements WatchListProvider { - private static final Logger logger = LoggerFactory.getLogger(RegexWatchListProvider.class); - - private final List includePatterns; - private final List excludePatterns; - private final ConsumerMetadata metadata; - - public RegexWatchListProvider(List includePatterns) { - this(includePatterns, Collections.emptyList()); + private static final Logger logger = LoggerFactory.getLogger(RegexWatchListProvider.class); + + private final List includePatterns; + private final List excludePatterns; + private final ConsumerMetadata metadata; + + public RegexWatchListProvider(List includePatterns) { + this(includePatterns, Collections.emptyList()); + } + + public RegexWatchListProvider(List includePatterns, List excludePatterns) { + this.includePatterns = compilePatterns(includePatterns); + this.excludePatterns = compilePatterns(excludePatterns); + this.metadata = ConsumerMetadata.builder().consumerType("regex").build(); + logger.info( + "Created RegexWatchListProvider with {} include patterns and {} exclude patterns", + this.includePatterns.size(), + this.excludePatterns.size()); + } + + private List compilePatterns(List patterns) { + if (patterns == null || patterns.isEmpty()) { + return Collections.emptyList(); } - - public RegexWatchListProvider(List includePatterns, List excludePatterns) { - this.includePatterns = compilePatterns(includePatterns); - this.excludePatterns = compilePatterns(excludePatterns); - this.metadata = ConsumerMetadata.builder() - .consumerType("regex") - .build(); - logger.info("Created RegexWatchListProvider with {} include patterns and {} exclude patterns", - this.includePatterns.size(), this.excludePatterns.size()); - } - - private List compilePatterns(List patterns) { - if (patterns == null || patterns.isEmpty()) { - return Collections.emptyList(); - } - List compiled = new ArrayList<>(patterns.size()); - for (String pattern : patterns) { - try { - compiled.add(Pattern.compile(pattern)); - } catch (Exception e) { - logger.warn("Invalid regex pattern: {}", pattern, e); - } - } - return compiled; + List compiled = new ArrayList<>(patterns.size()); + for (String pattern : patterns) { + try { + compiled.add(Pattern.compile(pattern)); + } catch (Exception e) { + logger.warn("Invalid regex pattern: {}", pattern, e); + } } - - @Override - public String getName() { - return "regex"; - } - - @Override - public Map> getGroupAndTopics() { - // Regex providers don't pre-enumerate - they filter dynamically - return Collections.emptyMap(); - } - - @Override - public Optional getMetadata(GroupAndTopic groupAndTopic) { - return contains(groupAndTopic) ? Optional.of(metadata) : Optional.empty(); + return compiled; + } + + @Override + public String getName() { + return "regex"; + } + + @Override + public Map> getGroupAndTopics() { + // Regex providers don't pre-enumerate - they filter dynamically + return Collections.emptyMap(); + } + + @Override + public Optional getMetadata(GroupAndTopic groupAndTopic) { + return contains(groupAndTopic) ? Optional.of(metadata) : Optional.empty(); + } + + @Override + public Map> getGroupAndTopicsMetadata() { + return Collections.emptyMap(); + } + + @Override + public void refresh() { + // Regex patterns are static + } + + @Override + public boolean contains(GroupAndTopic groupAndTopic) { + String groupId = groupAndTopic.getGroup(); + + // Check exclusions first + for (Pattern excludePattern : excludePatterns) { + if (excludePattern.matcher(groupId).matches()) { + return false; + } } - @Override - public Map> getGroupAndTopicsMetadata() { - return Collections.emptyMap(); + // If no include patterns, accept all (minus exclusions) + if (includePatterns.isEmpty()) { + return true; } - @Override - public void refresh() { - // Regex patterns are static + // Check inclusions + for (Pattern includePattern : includePatterns) { + if (includePattern.matcher(groupId).matches()) { + return true; + } } - @Override - public boolean contains(GroupAndTopic groupAndTopic) { - String groupId = groupAndTopic.getGroup(); - - // Check exclusions first - for (Pattern excludePattern : excludePatterns) { - if (excludePattern.matcher(groupId).matches()) { - return false; - } - } - - // If no include patterns, accept all (minus exclusions) - if (includePatterns.isEmpty()) { - return true; - } - - // Check inclusions - for (Pattern includePattern : includePatterns) { - if (includePattern.matcher(groupId).matches()) { - return true; - } - } + return false; + } - return false; - } - - @Override - public int getPriority() { - return 75; // Lower priority than static, higher than all-consumers - } + @Override + public int getPriority() { + return 75; // Lower priority than static, higher than all-consumers + } } diff --git a/src/main/java/com/uber/ugroup/watchlist/StaticWatchListProvider.java b/src/main/java/com/uber/ugroup/watchlist/StaticWatchListProvider.java index 91db19f..76831f3 100644 --- a/src/main/java/com/uber/ugroup/watchlist/StaticWatchListProvider.java +++ b/src/main/java/com/uber/ugroup/watchlist/StaticWatchListProvider.java @@ -17,12 +17,6 @@ import com.uber.ugroup.model.ConsumerMetadata; import com.uber.ugroup.model.GroupAndTopic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; - import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -33,11 +27,17 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; /** * WatchListProvider that loads consumer groups and topics from a YAML file. * - * Expected format: + *

Expected format: + * *

  * watchlist:
  *   - group: my-consumer-group
@@ -53,112 +53,115 @@
  */
 public class StaticWatchListProvider implements WatchListProvider {
 
-    private static final Logger logger = LoggerFactory.getLogger(StaticWatchListProvider.class);
-
-    private final String name;
-    private final String yamlFilePath;
-    private final int consumerOffsetsPartitionCount;
-    private volatile Map> groupAndTopicsByPartition;
-    private volatile Map> metadata;
-    private volatile Set allGroupAndTopics;
-
-    public StaticWatchListProvider(String yamlFilePath) {
-        this(yamlFilePath, 50);
-    }
-
-    public StaticWatchListProvider(String yamlFilePath, int consumerOffsetsPartitionCount) {
-        this.name = "static";
-        this.yamlFilePath = yamlFilePath;
-        this.consumerOffsetsPartitionCount = consumerOffsetsPartitionCount;
-        this.groupAndTopicsByPartition = new ConcurrentHashMap<>();
-        this.metadata = new ConcurrentHashMap<>();
-        this.allGroupAndTopics = ConcurrentHashMap.newKeySet();
-        refresh();
-    }
-
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    @Override
-    public Map> getGroupAndTopics() {
-        return groupAndTopicsByPartition;
+  private static final Logger logger = LoggerFactory.getLogger(StaticWatchListProvider.class);
+
+  private final String name;
+  private final String yamlFilePath;
+  private final int consumerOffsetsPartitionCount;
+  private volatile Map> groupAndTopicsByPartition;
+  private volatile Map> metadata;
+  private volatile Set allGroupAndTopics;
+
+  public StaticWatchListProvider(String yamlFilePath) {
+    this(yamlFilePath, 50);
+  }
+
+  public StaticWatchListProvider(String yamlFilePath, int consumerOffsetsPartitionCount) {
+    this.name = "static";
+    this.yamlFilePath = yamlFilePath;
+    this.consumerOffsetsPartitionCount = consumerOffsetsPartitionCount;
+    this.groupAndTopicsByPartition = new ConcurrentHashMap<>();
+    this.metadata = new ConcurrentHashMap<>();
+    this.allGroupAndTopics = ConcurrentHashMap.newKeySet();
+    refresh();
+  }
+
+  @Override
+  public String getName() {
+    return name;
+  }
+
+  @Override
+  public Map> getGroupAndTopics() {
+    return groupAndTopicsByPartition;
+  }
+
+  @Override
+  public Optional getMetadata(GroupAndTopic groupAndTopic) {
+    Map topicMap = metadata.get(groupAndTopic.getGroup());
+    if (topicMap != null) {
+      return Optional.ofNullable(topicMap.get(groupAndTopic.getTopic()));
     }
-
-    @Override
-    public Optional getMetadata(GroupAndTopic groupAndTopic) {
-        Map topicMap = metadata.get(groupAndTopic.getGroup());
-        if (topicMap != null) {
-            return Optional.ofNullable(topicMap.get(groupAndTopic.getTopic()));
-        }
-        return Optional.empty();
+    return Optional.empty();
+  }
+
+  @Override
+  public Map> getGroupAndTopicsMetadata() {
+    return metadata;
+  }
+
+  @Override
+  @SuppressWarnings("unchecked")
+  public void refresh() {
+    if (yamlFilePath == null || yamlFilePath.isEmpty()) {
+      logger.debug("No YAML file configured for static watchlist");
+      return;
     }
 
-    @Override
-    public Map> getGroupAndTopicsMetadata() {
-        return metadata;
-    }
-
-    @Override
-    @SuppressWarnings("unchecked")
-    public void refresh() {
-        if (yamlFilePath == null || yamlFilePath.isEmpty()) {
-            logger.debug("No YAML file configured for static watchlist");
-            return;
-        }
-
-        Map> newByPartition = new HashMap<>();
-        Map> newMetadata = new HashMap<>();
-        Set newAll = new HashSet<>();
-
-        Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
-        try (InputStream input = new FileInputStream(yamlFilePath)) {
-            Map root = yaml.load(input);
-            List> watchlist = (List>) root.get("watchlist");
-
-            if (watchlist != null) {
-                for (Map entry : watchlist) {
-                    String group = (String) entry.get("group");
-                    List topics = (List) entry.get("topics");
-                    Map metadataMap = (Map) entry.getOrDefault("metadata", Map.of());
-
-                    ConsumerMetadata.Builder metaBuilder = ConsumerMetadata.builder()
-                            .consumerType((String) metadataMap.getOrDefault("type", "static"));
-
-                    if (topics != null) {
-                        for (String topic : topics) {
-                            GroupAndTopic gat = new GroupAndTopic(group, topic)
-                                    .withPartitionCount(consumerOffsetsPartitionCount);
-
-                            int partition = gat.getConsumerOffsetsPartition();
-                            newByPartition.computeIfAbsent(partition, k -> new HashSet<>()).add(gat);
-                            newAll.add(gat);
-
-                            newMetadata.computeIfAbsent(group, k -> new HashMap<>())
-                                    .put(topic, metaBuilder.build());
-                        }
-                    }
-                }
+    Map> newByPartition = new HashMap<>();
+    Map> newMetadata = new HashMap<>();
+    Set newAll = new HashSet<>();
+
+    Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
+    try (InputStream input = new FileInputStream(yamlFilePath)) {
+      Map root = yaml.load(input);
+      List> watchlist = (List>) root.get("watchlist");
+
+      if (watchlist != null) {
+        for (Map entry : watchlist) {
+          String group = (String) entry.get("group");
+          List topics = (List) entry.get("topics");
+          Map metadataMap =
+              (Map) entry.getOrDefault("metadata", Map.of());
+
+          ConsumerMetadata.Builder metaBuilder =
+              ConsumerMetadata.builder()
+                  .consumerType((String) metadataMap.getOrDefault("type", "static"));
+
+          if (topics != null) {
+            for (String topic : topics) {
+              GroupAndTopic gat =
+                  new GroupAndTopic(group, topic).withPartitionCount(consumerOffsetsPartitionCount);
+
+              int partition = gat.getConsumerOffsetsPartition();
+              newByPartition.computeIfAbsent(partition, k -> new HashSet<>()).add(gat);
+              newAll.add(gat);
+
+              newMetadata
+                  .computeIfAbsent(group, k -> new HashMap<>())
+                  .put(topic, metaBuilder.build());
             }
+          }
+        }
+      }
 
-            this.groupAndTopicsByPartition = newByPartition;
-            this.metadata = newMetadata;
-            this.allGroupAndTopics = newAll;
-            logger.info("Loaded {} group-topic combinations from {}", newAll.size(), yamlFilePath);
+      this.groupAndTopicsByPartition = newByPartition;
+      this.metadata = newMetadata;
+      this.allGroupAndTopics = newAll;
+      logger.info("Loaded {} group-topic combinations from {}", newAll.size(), yamlFilePath);
 
-        } catch (IOException e) {
-            logger.error("Failed to load watchlist from {}", yamlFilePath, e);
-        }
+    } catch (IOException e) {
+      logger.error("Failed to load watchlist from {}", yamlFilePath, e);
     }
+  }
 
-    @Override
-    public boolean contains(GroupAndTopic groupAndTopic) {
-        return allGroupAndTopics.contains(groupAndTopic);
-    }
+  @Override
+  public boolean contains(GroupAndTopic groupAndTopic) {
+    return allGroupAndTopics.contains(groupAndTopic);
+  }
 
-    @Override
-    public int getPriority() {
-        return 50; // Higher priority than default, lower than specific providers
-    }
+  @Override
+  public int getPriority() {
+    return 50; // Higher priority than default, lower than specific providers
+  }
 }
diff --git a/src/main/java/com/uber/ugroup/watchlist/WatchListProvider.java b/src/main/java/com/uber/ugroup/watchlist/WatchListProvider.java
index c50ae0a..59d0a36 100644
--- a/src/main/java/com/uber/ugroup/watchlist/WatchListProvider.java
+++ b/src/main/java/com/uber/ugroup/watchlist/WatchListProvider.java
@@ -15,70 +15,66 @@
  */
 package com.uber.ugroup.watchlist;
 
-import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.ConsumerMetadata;
-
+import com.uber.ugroup.model.GroupAndTopic;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
 /**
- * Interface for providing the list of consumer groups and topics to monitor.
- * Allows pluggable sources like static files, databases, or APIs.
+ * Interface for providing the list of consumer groups and topics to monitor. Allows pluggable
+ * sources like static files, databases, or APIs.
  */
 public interface WatchListProvider {
 
-    /**
-     * Get the name/identifier of this watch list provider.
-     *
-     * @return provider name (e.g., "static", "proxy", "athena")
-     */
-    String getName();
+  /**
+   * Get the name/identifier of this watch list provider.
+   *
+   * @return provider name (e.g., "static", "proxy", "athena")
+   */
+  String getName();
 
-    /**
-     * Get all group-topic combinations to monitor, partitioned by __consumer_offsets partition.
-     * The key is the partition of the __consumer_offsets topic that contains commits for these groups.
-     *
-     * @return map of __consumer_offsets partition to set of group-topic combinations
-     */
-    Map> getGroupAndTopics();
+  /**
+   * Get all group-topic combinations to monitor, partitioned by __consumer_offsets partition. The
+   * key is the partition of the __consumer_offsets topic that contains commits for these groups.
+   *
+   * @return map of __consumer_offsets partition to set of group-topic combinations
+   */
+  Map> getGroupAndTopics();
 
-    /**
-     * Get metadata for a specific consumer group and topic.
-     *
-     * @param groupAndTopic the group and topic to get metadata for
-     * @return optional metadata if available
-     */
-    Optional getMetadata(GroupAndTopic groupAndTopic);
+  /**
+   * Get metadata for a specific consumer group and topic.
+   *
+   * @param groupAndTopic the group and topic to get metadata for
+   * @return optional metadata if available
+   */
+  Optional getMetadata(GroupAndTopic groupAndTopic);
 
-    /**
-     * Get all group-topics with their associated metadata.
-     *
-     * @return map of group name to map of topic to metadata
-     */
-    Map> getGroupAndTopicsMetadata();
+  /**
+   * Get all group-topics with their associated metadata.
+   *
+   * @return map of group name to map of topic to metadata
+   */
+  Map> getGroupAndTopicsMetadata();
 
-    /**
-     * Refresh the watch list from its source.
-     * Called periodically to pick up changes.
-     */
-    void refresh();
+  /** Refresh the watch list from its source. Called periodically to pick up changes. */
+  void refresh();
 
-    /**
-     * Check if this provider contains the given group-topic combination.
-     *
-     * @param groupAndTopic the group and topic to check
-     * @return true if this provider is responsible for monitoring this combination
-     */
-    boolean contains(GroupAndTopic groupAndTopic);
+  /**
+   * Check if this provider contains the given group-topic combination.
+   *
+   * @param groupAndTopic the group and topic to check
+   * @return true if this provider is responsible for monitoring this combination
+   */
+  boolean contains(GroupAndTopic groupAndTopic);
 
-    /**
-     * Get the priority of this provider. Lower numbers = higher priority.
-     * When multiple providers match a group-topic, the highest priority one is used.
-     *
-     * @return priority (default 100)
-     */
-    default int getPriority() {
-        return 100;
-    }
+  /**
+   * Get the priority of this provider. Lower numbers = higher priority. When multiple providers
+   * match a group-topic, the highest priority one is used.
+   *
+   * @return priority (default 100)
+   */
+  default int getPriority() {
+    return 100;
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/UGroupApplicationTest.java b/src/test/java/com/uber/ugroup/UGroupApplicationTest.java
index 4c0b609..17526ea 100644
--- a/src/test/java/com/uber/ugroup/UGroupApplicationTest.java
+++ b/src/test/java/com/uber/ugroup/UGroupApplicationTest.java
@@ -15,28 +15,26 @@
  */
 package com.uber.ugroup;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 class UGroupApplicationTest {
 
-    @Test
-    void hasSpringBootApplicationAnnotation() {
-        assertThat(UGroupApplication.class.isAnnotationPresent(SpringBootApplication.class))
-                .isTrue();
-    }
+  @Test
+  void hasSpringBootApplicationAnnotation() {
+    assertThat(UGroupApplication.class.isAnnotationPresent(SpringBootApplication.class)).isTrue();
+  }
 
-    @Test
-    void mainMethodExists() throws NoSuchMethodException {
-        assertThat(UGroupApplication.class.getMethod("main", String[].class))
-                .isNotNull();
-    }
+  @Test
+  void mainMethodExists() throws NoSuchMethodException {
+    assertThat(UGroupApplication.class.getMethod("main", String[].class)).isNotNull();
+  }
 
-    @Test
-    void classIsPublic() {
-        assertThat(java.lang.reflect.Modifier.isPublic(UGroupApplication.class.getModifiers()))
-                .isTrue();
-    }
+  @Test
+  void classIsPublic() {
+    assertThat(java.lang.reflect.Modifier.isPublic(UGroupApplication.class.getModifiers()))
+        .isTrue();
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/api/HealthControllerTest.java b/src/test/java/com/uber/ugroup/api/HealthControllerTest.java
index f4e5493..3ce3f10 100644
--- a/src/test/java/com/uber/ugroup/api/HealthControllerTest.java
+++ b/src/test/java/com/uber/ugroup/api/HealthControllerTest.java
@@ -15,6 +15,9 @@
  */
 package com.uber.ugroup.api;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
 import com.uber.ugroup.cache.OffsetCache;
 import com.uber.ugroup.config.UGroupProperties;
 import org.junit.jupiter.api.BeforeEach;
@@ -26,84 +29,80 @@
 import org.springframework.boot.actuate.health.Status;
 import org.springframework.http.ResponseEntity;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.when;
-
 @ExtendWith(MockitoExtension.class)
 class HealthControllerTest {
 
-    @Mock
-    private OffsetCache offsetCache;
+  @Mock private OffsetCache offsetCache;
 
-    private UGroupProperties properties;
-    private HealthController controller;
+  private UGroupProperties properties;
+  private HealthController controller;
 
-    @BeforeEach
-    void setUp() {
-        properties = new UGroupProperties();
-        properties.getKafka().setClusterName("test-cluster");
-        properties.getKafka().setBootstrapServers("broker1:9092");
-        controller = new HealthController(properties, offsetCache);
-    }
+  @BeforeEach
+  void setUp() {
+    properties = new UGroupProperties();
+    properties.getKafka().setClusterName("test-cluster");
+    properties.getKafka().setBootstrapServers("broker1:9092");
+    controller = new HealthController(properties, offsetCache);
+  }
 
-    @Test
-    void getStatus_returnsRunningStatus() {
-        when(offsetCache.size()).thenReturn(42L);
+  @Test
+  void getStatus_returnsRunningStatus() {
+    when(offsetCache.size()).thenReturn(42L);
 
-        ResponseEntity response = controller.getStatus();
+    ResponseEntity response = controller.getStatus();
 
-        assertThat(response.getStatusCode().value()).isEqualTo(200);
-        HealthController.StatusResponse body = response.getBody();
-        assertThat(body).isNotNull();
-        assertThat(body.status()).isEqualTo("running");
-        assertThat(body.cluster()).isEqualTo("test-cluster");
-        assertThat(body.bootstrapServers()).isEqualTo("broker1:9092");
-        assertThat(body.cacheSize()).isEqualTo(42L);
-        assertThat(body.uptimeMs()).isGreaterThanOrEqualTo(0);
-        assertThat(body.usedMemory()).isGreaterThan(0);
-        assertThat(body.maxMemory()).isGreaterThan(0);
-    }
+    assertThat(response.getStatusCode().value()).isEqualTo(200);
+    HealthController.StatusResponse body = response.getBody();
+    assertThat(body).isNotNull();
+    assertThat(body.status()).isEqualTo("running");
+    assertThat(body.cluster()).isEqualTo("test-cluster");
+    assertThat(body.bootstrapServers()).isEqualTo("broker1:9092");
+    assertThat(body.cacheSize()).isEqualTo(42L);
+    assertThat(body.uptimeMs()).isGreaterThanOrEqualTo(0);
+    assertThat(body.usedMemory()).isGreaterThan(0);
+    assertThat(body.maxMemory()).isGreaterThan(0);
+  }
 
-    @Test
-    void getStatus_uptimeIncreases() {
-        when(offsetCache.size()).thenReturn(0L);
+  @Test
+  void getStatus_uptimeIncreases() {
+    when(offsetCache.size()).thenReturn(0L);
 
-        ResponseEntity first = controller.getStatus();
-        ResponseEntity second = controller.getStatus();
+    ResponseEntity first = controller.getStatus();
+    ResponseEntity second = controller.getStatus();
 
-        assertThat(second.getBody().uptimeMs()).isGreaterThanOrEqualTo(first.getBody().uptimeMs());
-    }
+    assertThat(second.getBody().uptimeMs()).isGreaterThanOrEqualTo(first.getBody().uptimeMs());
+  }
 
-    @Test
-    void health_returnsUp() {
-        when(offsetCache.size()).thenReturn(10L);
+  @Test
+  void health_returnsUp() {
+    when(offsetCache.size()).thenReturn(10L);
 
-        Health health = controller.health();
+    Health health = controller.health();
 
-        assertThat(health.getStatus()).isEqualTo(Status.UP);
-        assertThat(health.getDetails()).containsEntry("cluster", "test-cluster");
-        assertThat(health.getDetails()).containsEntry("cacheSize", 10L);
-        assertThat(health.getDetails()).containsKey("uptime");
-    }
+    assertThat(health.getStatus()).isEqualTo(Status.UP);
+    assertThat(health.getDetails()).containsEntry("cluster", "test-cluster");
+    assertThat(health.getDetails()).containsEntry("cacheSize", 10L);
+    assertThat(health.getDetails()).containsKey("uptime");
+  }
 
-    @Test
-    void health_returnsDown_onException() {
-        when(offsetCache.size()).thenThrow(new RuntimeException("cache error"));
+  @Test
+  void health_returnsDown_onException() {
+    when(offsetCache.size()).thenThrow(new RuntimeException("cache error"));
 
-        Health health = controller.health();
+    Health health = controller.health();
 
-        assertThat(health.getStatus()).isEqualTo(Status.DOWN);
-    }
+    assertThat(health.getStatus()).isEqualTo(Status.DOWN);
+  }
 
-    @Test
-    void getStatus_withDefaultProperties() {
-        UGroupProperties defaults = new UGroupProperties();
-        HealthController defaultController = new HealthController(defaults, offsetCache);
-        when(offsetCache.size()).thenReturn(0L);
+  @Test
+  void getStatus_withDefaultProperties() {
+    UGroupProperties defaults = new UGroupProperties();
+    HealthController defaultController = new HealthController(defaults, offsetCache);
+    when(offsetCache.size()).thenReturn(0L);
 
-        ResponseEntity response = defaultController.getStatus();
+    ResponseEntity response = defaultController.getStatus();
 
-        assertThat(response.getBody().cluster()).isEqualTo("default");
-        assertThat(response.getBody().bootstrapServers()).isEqualTo("localhost:9092");
-    }
+    assertThat(response.getBody().cluster()).isEqualTo("default");
+    assertThat(response.getBody().bootstrapServers()).isEqualTo("localhost:9092");
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/api/LagControllerTest.java b/src/test/java/com/uber/ugroup/api/LagControllerTest.java
index 0fe1a00..66d92a7 100644
--- a/src/test/java/com/uber/ugroup/api/LagControllerTest.java
+++ b/src/test/java/com/uber/ugroup/api/LagControllerTest.java
@@ -15,8 +15,14 @@
  */
 package com.uber.ugroup.api;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
 import com.uber.ugroup.config.UGroupProperties;
 import com.uber.ugroup.fetcher.OffsetFetcher;
+import java.util.List;
+import java.util.Map;
 import org.apache.kafka.clients.consumer.OffsetAndMetadata;
 import org.apache.kafka.common.Node;
 import org.apache.kafka.common.PartitionInfo;
@@ -29,214 +35,204 @@
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.when;
-
 @ExtendWith(MockitoExtension.class)
 class LagControllerTest {
 
-    @Mock
-    private OffsetFetcher offsetFetcher;
+  @Mock private OffsetFetcher offsetFetcher;
 
-    private UGroupProperties properties;
-    private LagController controller;
+  private UGroupProperties properties;
+  private LagController controller;
 
-    private static final Node NODE = new Node(0, "localhost", 9092);
+  private static final Node NODE = new Node(0, "localhost", 9092);
 
-    @BeforeEach
-    void setUp() {
-        properties = new UGroupProperties();
-        properties.getKafka().setClusterName("test-cluster");
-        controller = new LagController(offsetFetcher, properties);
-    }
+  @BeforeEach
+  void setUp() {
+    properties = new UGroupProperties();
+    properties.getKafka().setClusterName("test-cluster");
+    controller = new LagController(offsetFetcher, properties);
+  }
 
-    @Test
-    void getLag_success() {
-        String group = "my-group";
-        String topic = "my-topic";
+  @Test
+  void getLag_success() {
+    String group = "my-group";
+    String topic = "my-topic";
 
-        List partitions = List.of(
-                new PartitionInfo(topic, 0, NODE, new Node[]{}, new Node[]{}),
-                new PartitionInfo(topic, 1, NODE, new Node[]{}, new Node[]{})
-        );
+    List partitions =
+        List.of(
+            new PartitionInfo(topic, 0, NODE, new Node[] {}, new Node[] {}),
+            new PartitionInfo(topic, 1, NODE, new Node[] {}, new Node[] {}));
 
-        TopicPartition tp0 = new TopicPartition(topic, 0);
-        TopicPartition tp1 = new TopicPartition(topic, 1);
+    TopicPartition tp0 = new TopicPartition(topic, 0);
+    TopicPartition tp1 = new TopicPartition(topic, 1);
 
-        when(offsetFetcher.partitionsFor(topic)).thenReturn(partitions);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L, tp1, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L, tp1, 200L));
-        when(offsetFetcher.listConsumerGroupOffsets(group)).thenReturn(Map.of(
+    when(offsetFetcher.partitionsFor(topic)).thenReturn(partitions);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L, tp1, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L, tp1, 200L));
+    when(offsetFetcher.listConsumerGroupOffsets(group))
+        .thenReturn(
+            Map.of(
                 tp0, new OffsetAndMetadata(90L),
-                tp1, new OffsetAndMetadata(150L)
-        ));
-
-        ResponseEntity response = controller.getLag(group, topic);
-
-        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-        assertThat(response.getBody()).isNotNull();
-        assertThat(response.getBody().group()).isEqualTo(group);
-        assertThat(response.getBody().topic()).isEqualTo(topic);
-        assertThat(response.getBody().cluster()).isEqualTo("test-cluster");
-        // Lag = (100-90) + (200-150) = 10 + 50 = 60
-        assertThat(response.getBody().totalLag()).isEqualTo(60L);
-        assertThat(response.getBody().partitions()).hasSize(2);
-        assertThat(response.getBody().partitions().get(0).lag()).isEqualTo(10L);
-        assertThat(response.getBody().partitions().get(1).lag()).isEqualTo(50L);
-    }
-
-    @Test
-    void getLag_noCommittedOffsets() {
-        String group = "new-group";
-        String topic = "my-topic";
-
-        List partitions = List.of(
-                new PartitionInfo(topic, 0, NODE, new Node[]{}, new Node[]{})
-        );
-
-        TopicPartition tp0 = new TopicPartition(topic, 0);
-
-        when(offsetFetcher.partitionsFor(topic)).thenReturn(partitions);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 10L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
-        when(offsetFetcher.listConsumerGroupOffsets(group)).thenReturn(Map.of());
-
-        ResponseEntity response = controller.getLag(group, topic);
-
-        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-        assertThat(response.getBody()).isNotNull();
-        // When no committed offset, lag = end - beginning = 100 - 10 = 90
-        assertThat(response.getBody().totalLag()).isEqualTo(90L);
-        assertThat(response.getBody().partitions().get(0).committedOffset()).isEqualTo(-1L);
-    }
-
-    @Test
-    void getLag_topicNotFound() {
-        when(offsetFetcher.partitionsFor("nonexistent")).thenReturn(null);
-
-        ResponseEntity response = controller.getLag("group", "nonexistent");
-
-        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
-        assertThat(response.getBody()).isNull();
-    }
-
-    @Test
-    void getLag_emptyPartitions() {
-        when(offsetFetcher.partitionsFor("empty-topic")).thenReturn(List.of());
-
-        ResponseEntity response = controller.getLag("group", "empty-topic");
-
-        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
-    }
-
-    @Test
-    void getLag_partitionLagDetails() {
-        String group = "my-group";
-        String topic = "my-topic";
-
-        List partitions = List.of(
-                new PartitionInfo(topic, 0, NODE, new Node[]{}, new Node[]{})
-        );
-
-        TopicPartition tp0 = new TopicPartition(topic, 0);
-
-        when(offsetFetcher.partitionsFor(topic)).thenReturn(partitions);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 5L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
-        when(offsetFetcher.listConsumerGroupOffsets(group)).thenReturn(Map.of(
-                tp0, new OffsetAndMetadata(80L)
-        ));
-
-        ResponseEntity response = controller.getLag(group, topic);
-
-        LagController.PartitionLag pLag = response.getBody().partitions().get(0);
-        assertThat(pLag.partition()).isEqualTo(0);
-        assertThat(pLag.beginningOffset()).isEqualTo(5L);
-        assertThat(pLag.committedOffset()).isEqualTo(80L);
-        assertThat(pLag.endOffset()).isEqualTo(100L);
-        assertThat(pLag.lag()).isEqualTo(20L);
-    }
-
-    @Test
-    void getLag_lagNeverNegative() {
-        String group = "my-group";
-        String topic = "my-topic";
-
-        List partitions = List.of(
-                new PartitionInfo(topic, 0, NODE, new Node[]{}, new Node[]{})
-        );
-
-        TopicPartition tp0 = new TopicPartition(topic, 0);
-
-        when(offsetFetcher.partitionsFor(topic)).thenReturn(partitions);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
-        // Committed offset is beyond end (can happen during compaction)
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 50L));
-        when(offsetFetcher.listConsumerGroupOffsets(group)).thenReturn(Map.of(
-                tp0, new OffsetAndMetadata(100L)
-        ));
-
-        ResponseEntity response = controller.getLag(group, topic);
-
-        assertThat(response.getBody().totalLag()).isEqualTo(0L);
-        assertThat(response.getBody().partitions().get(0).lag()).isEqualTo(0L);
-    }
-
-    @Test
-    void getGroupLag_success() {
-        String group = "my-group";
-        TopicPartition tp0 = new TopicPartition("topic-a", 0);
-        TopicPartition tp1 = new TopicPartition("topic-b", 0);
-
-        when(offsetFetcher.listConsumerGroupOffsets(group)).thenReturn(Map.of(
-                tp0, new OffsetAndMetadata(50L),
-                tp1, new OffsetAndMetadata(100L)
-        ));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L))
-                .thenReturn(Map.of(tp1, 200L));
+                tp1, new OffsetAndMetadata(150L)));
 
-        ResponseEntity response = controller.getGroupLag(group);
+    ResponseEntity response = controller.getLag(group, topic);
 
-        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-        assertThat(response.getBody()).isNotNull();
-        assertThat(response.getBody().group()).isEqualTo(group);
-        assertThat(response.getBody().cluster()).isEqualTo("test-cluster");
-        assertThat(response.getBody().topicLags()).hasSize(2);
-    }
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+    assertThat(response.getBody()).isNotNull();
+    assertThat(response.getBody().group()).isEqualTo(group);
+    assertThat(response.getBody().topic()).isEqualTo(topic);
+    assertThat(response.getBody().cluster()).isEqualTo("test-cluster");
+    // Lag = (100-90) + (200-150) = 10 + 50 = 60
+    assertThat(response.getBody().totalLag()).isEqualTo(60L);
+    assertThat(response.getBody().partitions()).hasSize(2);
+    assertThat(response.getBody().partitions().get(0).lag()).isEqualTo(10L);
+    assertThat(response.getBody().partitions().get(1).lag()).isEqualTo(50L);
+  }
+
+  @Test
+  void getLag_noCommittedOffsets() {
+    String group = "new-group";
+    String topic = "my-topic";
+
+    List partitions =
+        List.of(new PartitionInfo(topic, 0, NODE, new Node[] {}, new Node[] {}));
+
+    TopicPartition tp0 = new TopicPartition(topic, 0);
+
+    when(offsetFetcher.partitionsFor(topic)).thenReturn(partitions);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 10L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
+    when(offsetFetcher.listConsumerGroupOffsets(group)).thenReturn(Map.of());
+
+    ResponseEntity response = controller.getLag(group, topic);
+
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+    assertThat(response.getBody()).isNotNull();
+    // When no committed offset, lag = end - beginning = 100 - 10 = 90
+    assertThat(response.getBody().totalLag()).isEqualTo(90L);
+    assertThat(response.getBody().partitions().get(0).committedOffset()).isEqualTo(-1L);
+  }
+
+  @Test
+  void getLag_topicNotFound() {
+    when(offsetFetcher.partitionsFor("nonexistent")).thenReturn(null);
+
+    ResponseEntity response = controller.getLag("group", "nonexistent");
+
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+    assertThat(response.getBody()).isNull();
+  }
+
+  @Test
+  void getLag_emptyPartitions() {
+    when(offsetFetcher.partitionsFor("empty-topic")).thenReturn(List.of());
+
+    ResponseEntity response = controller.getLag("group", "empty-topic");
+
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+  }
+
+  @Test
+  void getLag_partitionLagDetails() {
+    String group = "my-group";
+    String topic = "my-topic";
+
+    List partitions =
+        List.of(new PartitionInfo(topic, 0, NODE, new Node[] {}, new Node[] {}));
+
+    TopicPartition tp0 = new TopicPartition(topic, 0);
+
+    when(offsetFetcher.partitionsFor(topic)).thenReturn(partitions);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 5L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
+    when(offsetFetcher.listConsumerGroupOffsets(group))
+        .thenReturn(Map.of(tp0, new OffsetAndMetadata(80L)));
+
+    ResponseEntity response = controller.getLag(group, topic);
+
+    LagController.PartitionLag pLag = response.getBody().partitions().get(0);
+    assertThat(pLag.partition()).isEqualTo(0);
+    assertThat(pLag.beginningOffset()).isEqualTo(5L);
+    assertThat(pLag.committedOffset()).isEqualTo(80L);
+    assertThat(pLag.endOffset()).isEqualTo(100L);
+    assertThat(pLag.lag()).isEqualTo(20L);
+  }
+
+  @Test
+  void getLag_lagNeverNegative() {
+    String group = "my-group";
+    String topic = "my-topic";
+
+    List partitions =
+        List.of(new PartitionInfo(topic, 0, NODE, new Node[] {}, new Node[] {}));
+
+    TopicPartition tp0 = new TopicPartition(topic, 0);
+
+    when(offsetFetcher.partitionsFor(topic)).thenReturn(partitions);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
+    // Committed offset is beyond end (can happen during compaction)
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 50L));
+    when(offsetFetcher.listConsumerGroupOffsets(group))
+        .thenReturn(Map.of(tp0, new OffsetAndMetadata(100L)));
+
+    ResponseEntity response = controller.getLag(group, topic);
+
+    assertThat(response.getBody().totalLag()).isEqualTo(0L);
+    assertThat(response.getBody().partitions().get(0).lag()).isEqualTo(0L);
+  }
+
+  @Test
+  void getGroupLag_success() {
+    String group = "my-group";
+    TopicPartition tp0 = new TopicPartition("topic-a", 0);
+    TopicPartition tp1 = new TopicPartition("topic-b", 0);
 
-    @Test
-    void getGroupLag_noCommittedOffsets() {
-        when(offsetFetcher.listConsumerGroupOffsets("unknown-group")).thenReturn(Map.of());
-
-        ResponseEntity response = controller.getGroupLag("unknown-group");
-
-        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
-        assertThat(response.getBody()).isNull();
-    }
-
-    @Test
-    void getGroupLag_multiplePartitionsPerTopic() {
-        String group = "my-group";
-        TopicPartition tp0 = new TopicPartition("topic-a", 0);
-        TopicPartition tp1 = new TopicPartition("topic-a", 1);
-
-        when(offsetFetcher.listConsumerGroupOffsets(group)).thenReturn(Map.of(
+    when(offsetFetcher.listConsumerGroupOffsets(group))
+        .thenReturn(
+            Map.of(
+                tp0, new OffsetAndMetadata(50L),
+                tp1, new OffsetAndMetadata(100L)));
+    when(offsetFetcher.endOffsets(any()))
+        .thenReturn(Map.of(tp0, 100L))
+        .thenReturn(Map.of(tp1, 200L));
+
+    ResponseEntity response = controller.getGroupLag(group);
+
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+    assertThat(response.getBody()).isNotNull();
+    assertThat(response.getBody().group()).isEqualTo(group);
+    assertThat(response.getBody().cluster()).isEqualTo("test-cluster");
+    assertThat(response.getBody().topicLags()).hasSize(2);
+  }
+
+  @Test
+  void getGroupLag_noCommittedOffsets() {
+    when(offsetFetcher.listConsumerGroupOffsets("unknown-group")).thenReturn(Map.of());
+
+    ResponseEntity response =
+        controller.getGroupLag("unknown-group");
+
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+    assertThat(response.getBody()).isNull();
+  }
+
+  @Test
+  void getGroupLag_multiplePartitionsPerTopic() {
+    String group = "my-group";
+    TopicPartition tp0 = new TopicPartition("topic-a", 0);
+    TopicPartition tp1 = new TopicPartition("topic-a", 1);
+
+    when(offsetFetcher.listConsumerGroupOffsets(group))
+        .thenReturn(
+            Map.of(
                 tp0, new OffsetAndMetadata(80L),
-                tp1, new OffsetAndMetadata(90L)
-        ));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L, tp1, 100L));
+                tp1, new OffsetAndMetadata(90L)));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L, tp1, 100L));
 
-        ResponseEntity response = controller.getGroupLag(group);
+    ResponseEntity response = controller.getGroupLag(group);
 
-        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-        // Lag = (100-80) + (100-90) = 20 + 10 = 30
-        assertThat(response.getBody().totalLag()).isEqualTo(30L);
-        assertThat(response.getBody().topicLags()).containsEntry("topic-a", 30L);
-    }
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+    // Lag = (100-80) + (100-90) = 20 + 10 = 30
+    assertThat(response.getBody().totalLag()).isEqualTo(30L);
+    assertThat(response.getBody().topicLags()).containsEntry("topic-a", 30L);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/cache/CaffeineOffsetCacheTest.java b/src/test/java/com/uber/ugroup/cache/CaffeineOffsetCacheTest.java
index edec7db..66a950c 100644
--- a/src/test/java/com/uber/ugroup/cache/CaffeineOffsetCacheTest.java
+++ b/src/test/java/com/uber/ugroup/cache/CaffeineOffsetCacheTest.java
@@ -15,104 +15,103 @@
  */
 package com.uber.ugroup.cache;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
 import java.time.Duration;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 class CaffeineOffsetCacheTest {
 
-    private CaffeineOffsetCache cache;
-
-    @BeforeEach
-    void setUp() {
-        cache = new CaffeineOffsetCache(100, Duration.ofMinutes(5));
-    }
-
-    @Test
-    void putAndGet() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(100, System.currentTimeMillis()),
-                1, new LastCommittedOffset(200, System.currentTimeMillis())
-        );
-
-        cache.put(gat, offsets);
-
-        Optional> result = cache.get(gat);
-        assertThat(result).isPresent();
-        assertThat(result.get()).hasSize(2);
-        assertThat(result.get().get(0).getOffset()).isEqualTo(100);
-        assertThat(result.get().get(1).getOffset()).isEqualTo(200);
-    }
-
-    @Test
-    void get_notPresent() {
-        GroupAndTopic gat = new GroupAndTopic("nonexistent", "topic");
-        Optional> result = cache.get(gat);
-        assertThat(result).isEmpty();
+  private CaffeineOffsetCache cache;
+
+  @BeforeEach
+  void setUp() {
+    cache = new CaffeineOffsetCache(100, Duration.ofMinutes(5));
+  }
+
+  @Test
+  void putAndGet() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Map offsets =
+        Map.of(
+            0, new LastCommittedOffset(100, System.currentTimeMillis()),
+            1, new LastCommittedOffset(200, System.currentTimeMillis()));
+
+    cache.put(gat, offsets);
+
+    Optional> result = cache.get(gat);
+    assertThat(result).isPresent();
+    assertThat(result.get()).hasSize(2);
+    assertThat(result.get().get(0).getOffset()).isEqualTo(100);
+    assertThat(result.get().get(1).getOffset()).isEqualTo(200);
+  }
+
+  @Test
+  void get_notPresent() {
+    GroupAndTopic gat = new GroupAndTopic("nonexistent", "topic");
+    Optional> result = cache.get(gat);
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void invalidate() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    cache.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
+
+    cache.invalidate(gat);
+
+    assertThat(cache.get(gat)).isEmpty();
+  }
+
+  @Test
+  void invalidateExcept() {
+    // Create entries for different partitions
+    GroupAndTopic gat1 = new GroupAndTopic("group-in-partition-0", "topic");
+    GroupAndTopic gat2 = new GroupAndTopic("group-in-partition-1", "topic");
+
+    cache.put(gat1, Map.of(0, LastCommittedOffset.now(100)));
+    cache.put(gat2, Map.of(0, LastCommittedOffset.now(200)));
+
+    // Keep only entries in partition 0
+    Set retained = Set.of(gat1.getConsumerOffsetsPartition());
+    cache.invalidateExcept(retained);
+
+    // gat1 should still be there if its partition is retained
+    if (retained.contains(gat1.getConsumerOffsetsPartition())) {
+      assertThat(cache.get(gat1)).isPresent();
     }
+  }
 
-    @Test
-    void invalidate() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        cache.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
-
-        cache.invalidate(gat);
-
-        assertThat(cache.get(gat)).isEmpty();
-    }
+  @Test
+  void clear() {
+    cache.put(new GroupAndTopic("g1", "t1"), Map.of(0, LastCommittedOffset.now(100)));
+    cache.put(new GroupAndTopic("g2", "t2"), Map.of(0, LastCommittedOffset.now(200)));
 
-    @Test
-    void invalidateExcept() {
-        // Create entries for different partitions
-        GroupAndTopic gat1 = new GroupAndTopic("group-in-partition-0", "topic");
-        GroupAndTopic gat2 = new GroupAndTopic("group-in-partition-1", "topic");
+    assertThat(cache.size()).isEqualTo(2);
 
-        cache.put(gat1, Map.of(0, LastCommittedOffset.now(100)));
-        cache.put(gat2, Map.of(0, LastCommittedOffset.now(200)));
-
-        // Keep only entries in partition 0
-        Set retained = Set.of(gat1.getConsumerOffsetsPartition());
-        cache.invalidateExcept(retained);
-
-        // gat1 should still be there if its partition is retained
-        if (retained.contains(gat1.getConsumerOffsetsPartition())) {
-            assertThat(cache.get(gat1)).isPresent();
-        }
-    }
+    cache.clear();
 
-    @Test
-    void clear() {
-        cache.put(new GroupAndTopic("g1", "t1"), Map.of(0, LastCommittedOffset.now(100)));
-        cache.put(new GroupAndTopic("g2", "t2"), Map.of(0, LastCommittedOffset.now(200)));
+    assertThat(cache.size()).isEqualTo(0);
+  }
 
-        assertThat(cache.size()).isEqualTo(2);
+  @Test
+  void offsetIndex() {
+    cache.putOffsetIndex("topic", 0, 1000, 123456789L);
 
-        cache.clear();
+    Optional timestamp = cache.getTimestampForOffset("topic", 0, 1000);
+    assertThat(timestamp).isPresent();
+    assertThat(timestamp.get()).isEqualTo(123456789L);
+  }
 
-        assertThat(cache.size()).isEqualTo(0);
-    }
-
-    @Test
-    void offsetIndex() {
-        cache.putOffsetIndex("topic", 0, 1000, 123456789L);
-
-        Optional timestamp = cache.getTimestampForOffset("topic", 0, 1000);
-        assertThat(timestamp).isPresent();
-        assertThat(timestamp.get()).isEqualTo(123456789L);
-    }
-
-    @Test
-    void offsetIndex_notPresent() {
-        Optional timestamp = cache.getTimestampForOffset("topic", 0, 999);
-        assertThat(timestamp).isEmpty();
-    }
+  @Test
+  void offsetIndex_notPresent() {
+    Optional timestamp = cache.getTimestampForOffset("topic", 0, 999);
+    assertThat(timestamp).isEmpty();
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/cache/InMemoryOffsetCacheTest.java b/src/test/java/com/uber/ugroup/cache/InMemoryOffsetCacheTest.java
index dd9aec1..182414a 100644
--- a/src/test/java/com/uber/ugroup/cache/InMemoryOffsetCacheTest.java
+++ b/src/test/java/com/uber/ugroup/cache/InMemoryOffsetCacheTest.java
@@ -15,202 +15,201 @@
  */
 package com.uber.ugroup.cache;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 class InMemoryOffsetCacheTest {
 
-    private InMemoryOffsetCache cache;
-
-    @BeforeEach
-    void setUp() {
-        cache = new InMemoryOffsetCache();
-    }
-
-    @Test
-    void putAndGet() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(100, System.currentTimeMillis()),
-                1, new LastCommittedOffset(200, System.currentTimeMillis())
-        );
-
-        cache.put(gat, offsets);
-
-        Optional> result = cache.get(gat);
-        assertThat(result).isPresent();
-        assertThat(result.get()).hasSize(2);
-        assertThat(result.get().get(0).getOffset()).isEqualTo(100);
-        assertThat(result.get().get(1).getOffset()).isEqualTo(200);
-    }
-
-    @Test
-    void get_notPresent() {
-        GroupAndTopic gat = new GroupAndTopic("nonexistent", "topic");
-        Optional> result = cache.get(gat);
-        assertThat(result).isEmpty();
-    }
+  private InMemoryOffsetCache cache;
 
-    @Test
-    void put_overwritesExisting() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        cache.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
-        cache.put(gat, Map.of(0, new LastCommittedOffset(200, 0)));
+  @BeforeEach
+  void setUp() {
+    cache = new InMemoryOffsetCache();
+  }
+
+  @Test
+  void putAndGet() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Map offsets =
+        Map.of(
+            0, new LastCommittedOffset(100, System.currentTimeMillis()),
+            1, new LastCommittedOffset(200, System.currentTimeMillis()));
 
-        Optional> result = cache.get(gat);
-        assertThat(result).isPresent();
-        assertThat(result.get().get(0).getOffset()).isEqualTo(200);
-    }
+    cache.put(gat, offsets);
 
-    @Test
-    void put_storesImmutableCopy() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        java.util.HashMap mutableMap = new java.util.HashMap<>();
-        mutableMap.put(0, new LastCommittedOffset(100, 0));
+    Optional> result = cache.get(gat);
+    assertThat(result).isPresent();
+    assertThat(result.get()).hasSize(2);
+    assertThat(result.get().get(0).getOffset()).isEqualTo(100);
+    assertThat(result.get().get(1).getOffset()).isEqualTo(200);
+  }
 
-        cache.put(gat, mutableMap);
-        mutableMap.put(1, new LastCommittedOffset(200, 0));
+  @Test
+  void get_notPresent() {
+    GroupAndTopic gat = new GroupAndTopic("nonexistent", "topic");
+    Optional> result = cache.get(gat);
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void put_overwritesExisting() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    cache.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
+    cache.put(gat, Map.of(0, new LastCommittedOffset(200, 0)));
 
-        Optional> result = cache.get(gat);
-        assertThat(result).isPresent();
-        assertThat(result.get()).hasSize(1);
-    }
+    Optional> result = cache.get(gat);
+    assertThat(result).isPresent();
+    assertThat(result.get().get(0).getOffset()).isEqualTo(200);
+  }
 
-    @Test
-    void invalidate() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        cache.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
+  @Test
+  void put_storesImmutableCopy() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    java.util.HashMap mutableMap = new java.util.HashMap<>();
+    mutableMap.put(0, new LastCommittedOffset(100, 0));
+
+    cache.put(gat, mutableMap);
+    mutableMap.put(1, new LastCommittedOffset(200, 0));
+
+    Optional> result = cache.get(gat);
+    assertThat(result).isPresent();
+    assertThat(result.get()).hasSize(1);
+  }
+
+  @Test
+  void invalidate() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    cache.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
+
+    cache.invalidate(gat);
+
+    assertThat(cache.get(gat)).isEmpty();
+  }
+
+  @Test
+  void invalidate_notPresent_doesNotThrow() {
+    GroupAndTopic gat = new GroupAndTopic("nonexistent", "topic");
+    cache.invalidate(gat);
+  }
+
+  @Test
+  void invalidateExcept() {
+    GroupAndTopic gat1 = new GroupAndTopic("group1", "topic");
+    GroupAndTopic gat2 = new GroupAndTopic("group2", "topic");
+
+    cache.put(gat1, Map.of(0, LastCommittedOffset.now(100)));
+    cache.put(gat2, Map.of(0, LastCommittedOffset.now(200)));
+
+    int partition1 = gat1.getConsumerOffsetsPartition();
+    int partition2 = gat2.getConsumerOffsetsPartition();
+
+    // Retain only partition of gat1
+    cache.invalidateExcept(Set.of(partition1));
+
+    assertThat(cache.get(gat1)).isPresent();
+    if (partition1 != partition2) {
+      assertThat(cache.get(gat2)).isEmpty();
+    }
+  }
+
+  @Test
+  void invalidateExcept_emptyRetained_clearsAll() {
+    cache.put(new GroupAndTopic("g1", "t1"), Map.of(0, LastCommittedOffset.now(100)));
+    cache.put(new GroupAndTopic("g2", "t2"), Map.of(0, LastCommittedOffset.now(200)));
+
+    cache.invalidateExcept(Set.of());
+
+    assertThat(cache.size()).isEqualTo(0);
+  }
+
+  @Test
+  void clear() {
+    cache.put(new GroupAndTopic("g1", "t1"), Map.of(0, LastCommittedOffset.now(100)));
+    cache.put(new GroupAndTopic("g2", "t2"), Map.of(0, LastCommittedOffset.now(200)));
+
+    assertThat(cache.size()).isEqualTo(2);
+
+    cache.clear();
+
+    assertThat(cache.size()).isEqualTo(0);
+  }
+
+  @Test
+  void clear_alsoClearsTimestampIndex() {
+    cache.putOffsetIndex("topic", 0, 1000, 123456789L);
+    cache.clear();
+
+    assertThat(cache.getTimestampForOffset("topic", 0, 1000)).isEmpty();
+  }
+
+  @Test
+  void size_empty() {
+    assertThat(cache.size()).isEqualTo(0);
+  }
+
+  @Test
+  void size_afterPuts() {
+    cache.put(new GroupAndTopic("g1", "t1"), Map.of(0, LastCommittedOffset.now(100)));
+    assertThat(cache.size()).isEqualTo(1);
+
+    cache.put(new GroupAndTopic("g2", "t2"), Map.of(0, LastCommittedOffset.now(200)));
+    assertThat(cache.size()).isEqualTo(2);
+  }
 
-        cache.invalidate(gat);
+  @Test
+  void size_afterInvalidate() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    cache.put(gat, Map.of(0, LastCommittedOffset.now(100)));
+    cache.invalidate(gat);
 
-        assertThat(cache.get(gat)).isEmpty();
-    }
+    assertThat(cache.size()).isEqualTo(0);
+  }
 
-    @Test
-    void invalidate_notPresent_doesNotThrow() {
-        GroupAndTopic gat = new GroupAndTopic("nonexistent", "topic");
-        cache.invalidate(gat);
-    }
+  @Test
+  void putOffsetIndex_andGet() {
+    cache.putOffsetIndex("topic", 0, 1000, 123456789L);
 
-    @Test
-    void invalidateExcept() {
-        GroupAndTopic gat1 = new GroupAndTopic("group1", "topic");
-        GroupAndTopic gat2 = new GroupAndTopic("group2", "topic");
+    Optional timestamp = cache.getTimestampForOffset("topic", 0, 1000);
+    assertThat(timestamp).isPresent();
+    assertThat(timestamp.get()).isEqualTo(123456789L);
+  }
 
-        cache.put(gat1, Map.of(0, LastCommittedOffset.now(100)));
-        cache.put(gat2, Map.of(0, LastCommittedOffset.now(200)));
+  @Test
+  void getTimestampForOffset_notPresent() {
+    Optional timestamp = cache.getTimestampForOffset("topic", 0, 999);
+    assertThat(timestamp).isEmpty();
+  }
 
-        int partition1 = gat1.getConsumerOffsetsPartition();
-        int partition2 = gat2.getConsumerOffsetsPartition();
+  @Test
+  void putOffsetIndex_differentPartitions() {
+    cache.putOffsetIndex("topic", 0, 1000, 111L);
+    cache.putOffsetIndex("topic", 1, 1000, 222L);
 
-        // Retain only partition of gat1
-        cache.invalidateExcept(Set.of(partition1));
+    assertThat(cache.getTimestampForOffset("topic", 0, 1000).get()).isEqualTo(111L);
+    assertThat(cache.getTimestampForOffset("topic", 1, 1000).get()).isEqualTo(222L);
+  }
 
-        assertThat(cache.get(gat1)).isPresent();
-        if (partition1 != partition2) {
-            assertThat(cache.get(gat2)).isEmpty();
-        }
-    }
+  @Test
+  void putOffsetIndex_differentTopics() {
+    cache.putOffsetIndex("topic-a", 0, 1000, 111L);
+    cache.putOffsetIndex("topic-b", 0, 1000, 222L);
 
-    @Test
-    void invalidateExcept_emptyRetained_clearsAll() {
-        cache.put(new GroupAndTopic("g1", "t1"), Map.of(0, LastCommittedOffset.now(100)));
-        cache.put(new GroupAndTopic("g2", "t2"), Map.of(0, LastCommittedOffset.now(200)));
+    assertThat(cache.getTimestampForOffset("topic-a", 0, 1000).get()).isEqualTo(111L);
+    assertThat(cache.getTimestampForOffset("topic-b", 0, 1000).get()).isEqualTo(222L);
+  }
 
-        cache.invalidateExcept(Set.of());
+  @Test
+  void putOffsetIndex_overwritesExisting() {
+    cache.putOffsetIndex("topic", 0, 1000, 111L);
+    cache.putOffsetIndex("topic", 0, 1000, 222L);
 
-        assertThat(cache.size()).isEqualTo(0);
-    }
-
-    @Test
-    void clear() {
-        cache.put(new GroupAndTopic("g1", "t1"), Map.of(0, LastCommittedOffset.now(100)));
-        cache.put(new GroupAndTopic("g2", "t2"), Map.of(0, LastCommittedOffset.now(200)));
-
-        assertThat(cache.size()).isEqualTo(2);
-
-        cache.clear();
-
-        assertThat(cache.size()).isEqualTo(0);
-    }
-
-    @Test
-    void clear_alsoClearsTimestampIndex() {
-        cache.putOffsetIndex("topic", 0, 1000, 123456789L);
-        cache.clear();
-
-        assertThat(cache.getTimestampForOffset("topic", 0, 1000)).isEmpty();
-    }
-
-    @Test
-    void size_empty() {
-        assertThat(cache.size()).isEqualTo(0);
-    }
-
-    @Test
-    void size_afterPuts() {
-        cache.put(new GroupAndTopic("g1", "t1"), Map.of(0, LastCommittedOffset.now(100)));
-        assertThat(cache.size()).isEqualTo(1);
-
-        cache.put(new GroupAndTopic("g2", "t2"), Map.of(0, LastCommittedOffset.now(200)));
-        assertThat(cache.size()).isEqualTo(2);
-    }
-
-    @Test
-    void size_afterInvalidate() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        cache.put(gat, Map.of(0, LastCommittedOffset.now(100)));
-        cache.invalidate(gat);
-
-        assertThat(cache.size()).isEqualTo(0);
-    }
-
-    @Test
-    void putOffsetIndex_andGet() {
-        cache.putOffsetIndex("topic", 0, 1000, 123456789L);
-
-        Optional timestamp = cache.getTimestampForOffset("topic", 0, 1000);
-        assertThat(timestamp).isPresent();
-        assertThat(timestamp.get()).isEqualTo(123456789L);
-    }
-
-    @Test
-    void getTimestampForOffset_notPresent() {
-        Optional timestamp = cache.getTimestampForOffset("topic", 0, 999);
-        assertThat(timestamp).isEmpty();
-    }
-
-    @Test
-    void putOffsetIndex_differentPartitions() {
-        cache.putOffsetIndex("topic", 0, 1000, 111L);
-        cache.putOffsetIndex("topic", 1, 1000, 222L);
-
-        assertThat(cache.getTimestampForOffset("topic", 0, 1000).get()).isEqualTo(111L);
-        assertThat(cache.getTimestampForOffset("topic", 1, 1000).get()).isEqualTo(222L);
-    }
-
-    @Test
-    void putOffsetIndex_differentTopics() {
-        cache.putOffsetIndex("topic-a", 0, 1000, 111L);
-        cache.putOffsetIndex("topic-b", 0, 1000, 222L);
-
-        assertThat(cache.getTimestampForOffset("topic-a", 0, 1000).get()).isEqualTo(111L);
-        assertThat(cache.getTimestampForOffset("topic-b", 0, 1000).get()).isEqualTo(222L);
-    }
-
-    @Test
-    void putOffsetIndex_overwritesExisting() {
-        cache.putOffsetIndex("topic", 0, 1000, 111L);
-        cache.putOffsetIndex("topic", 0, 1000, 222L);
-
-        assertThat(cache.getTimestampForOffset("topic", 0, 1000).get()).isEqualTo(222L);
-    }
+    assertThat(cache.getTimestampForOffset("topic", 0, 1000).get()).isEqualTo(222L);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/cache/NoopOffsetCacheTest.java b/src/test/java/com/uber/ugroup/cache/NoopOffsetCacheTest.java
index bba25e9..f4dda01 100644
--- a/src/test/java/com/uber/ugroup/cache/NoopOffsetCacheTest.java
+++ b/src/test/java/com/uber/ugroup/cache/NoopOffsetCacheTest.java
@@ -15,87 +15,86 @@
  */
 package com.uber.ugroup.cache;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
-import org.junit.jupiter.api.Test;
-
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
 
 class NoopOffsetCacheTest {
 
-    @Test
-    void singletonInstance() {
-        assertThat(NoopOffsetCache.INSTANCE).isNotNull();
-        assertThat(NoopOffsetCache.INSTANCE).isSameAs(NoopOffsetCache.INSTANCE);
-    }
-
-    @Test
-    void singletonInstance_isOffsetCache() {
-        assertThat(NoopOffsetCache.INSTANCE).isInstanceOf(OffsetCache.class);
-    }
-
-    @Test
-    void get_returnsEmpty() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Optional> result = NoopOffsetCache.INSTANCE.get(gat);
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    void get_afterPut_returnsEmpty() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        NoopOffsetCache.INSTANCE.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
-
-        Optional> result = NoopOffsetCache.INSTANCE.get(gat);
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    void put_doesNotThrow() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        NoopOffsetCache.INSTANCE.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
-    }
-
-    @Test
-    void invalidate_doesNotThrow() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        NoopOffsetCache.INSTANCE.invalidate(gat);
-    }
-
-    @Test
-    void invalidateExcept_doesNotThrow() {
-        NoopOffsetCache.INSTANCE.invalidateExcept(Set.of(0, 1, 2));
-    }
-
-    @Test
-    void clear_doesNotThrow() {
-        NoopOffsetCache.INSTANCE.clear();
-    }
-
-    @Test
-    void size_returnsZero() {
-        assertThat(NoopOffsetCache.INSTANCE.size()).isEqualTo(0);
-    }
-
-    @Test
-    void putOffsetIndex_doesNotThrow() {
-        NoopOffsetCache.INSTANCE.putOffsetIndex("topic", 0, 1000, 123456789L);
-    }
-
-    @Test
-    void getTimestampForOffset_returnsEmpty() {
-        Optional result = NoopOffsetCache.INSTANCE.getTimestampForOffset("topic", 0, 1000);
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    void getTimestampForOffset_afterPut_returnsEmpty() {
-        NoopOffsetCache.INSTANCE.putOffsetIndex("topic", 0, 1000, 123456789L);
-        Optional result = NoopOffsetCache.INSTANCE.getTimestampForOffset("topic", 0, 1000);
-        assertThat(result).isEmpty();
-    }
+  @Test
+  void singletonInstance() {
+    assertThat(NoopOffsetCache.INSTANCE).isNotNull();
+    assertThat(NoopOffsetCache.INSTANCE).isSameAs(NoopOffsetCache.INSTANCE);
+  }
+
+  @Test
+  void singletonInstance_isOffsetCache() {
+    assertThat(NoopOffsetCache.INSTANCE).isInstanceOf(OffsetCache.class);
+  }
+
+  @Test
+  void get_returnsEmpty() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Optional> result = NoopOffsetCache.INSTANCE.get(gat);
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void get_afterPut_returnsEmpty() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    NoopOffsetCache.INSTANCE.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
+
+    Optional> result = NoopOffsetCache.INSTANCE.get(gat);
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void put_doesNotThrow() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    NoopOffsetCache.INSTANCE.put(gat, Map.of(0, new LastCommittedOffset(100, 0)));
+  }
+
+  @Test
+  void invalidate_doesNotThrow() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    NoopOffsetCache.INSTANCE.invalidate(gat);
+  }
+
+  @Test
+  void invalidateExcept_doesNotThrow() {
+    NoopOffsetCache.INSTANCE.invalidateExcept(Set.of(0, 1, 2));
+  }
+
+  @Test
+  void clear_doesNotThrow() {
+    NoopOffsetCache.INSTANCE.clear();
+  }
+
+  @Test
+  void size_returnsZero() {
+    assertThat(NoopOffsetCache.INSTANCE.size()).isEqualTo(0);
+  }
+
+  @Test
+  void putOffsetIndex_doesNotThrow() {
+    NoopOffsetCache.INSTANCE.putOffsetIndex("topic", 0, 1000, 123456789L);
+  }
+
+  @Test
+  void getTimestampForOffset_returnsEmpty() {
+    Optional result = NoopOffsetCache.INSTANCE.getTimestampForOffset("topic", 0, 1000);
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void getTimestampForOffset_afterPut_returnsEmpty() {
+    NoopOffsetCache.INSTANCE.putOffsetIndex("topic", 0, 1000, 123456789L);
+    Optional result = NoopOffsetCache.INSTANCE.getTimestampForOffset("topic", 0, 1000);
+    assertThat(result).isEmpty();
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/config/EnvironmentClusterConfigTest.java b/src/test/java/com/uber/ugroup/config/EnvironmentClusterConfigTest.java
index 51f1241..24e80d1 100644
--- a/src/test/java/com/uber/ugroup/config/EnvironmentClusterConfigTest.java
+++ b/src/test/java/com/uber/ugroup/config/EnvironmentClusterConfigTest.java
@@ -15,78 +15,78 @@
  */
 package com.uber.ugroup.config;
 
-import org.junit.jupiter.api.Test;
-
 import static org.assertj.core.api.Assertions.assertThat;
 
+import org.junit.jupiter.api.Test;
+
 class EnvironmentClusterConfigTest {
 
-    private final EnvironmentClusterConfig config = new EnvironmentClusterConfig();
+  private final EnvironmentClusterConfig config = new EnvironmentClusterConfig();
 
-    @Test
-    void getClusterName_defaultValue() {
-        // Without UGROUP_CLUSTER_NAME env var set, should return "default"
-        String envValue = System.getenv("UGROUP_CLUSTER_NAME");
-        if (envValue == null) {
-            assertThat(config.getClusterName()).isEqualTo("default");
-        } else {
-            assertThat(config.getClusterName()).isEqualTo(envValue);
-        }
+  @Test
+  void getClusterName_defaultValue() {
+    // Without UGROUP_CLUSTER_NAME env var set, should return "default"
+    String envValue = System.getenv("UGROUP_CLUSTER_NAME");
+    if (envValue == null) {
+      assertThat(config.getClusterName()).isEqualTo("default");
+    } else {
+      assertThat(config.getClusterName()).isEqualTo(envValue);
     }
+  }
 
-    @Test
-    void getBootstrapServers_defaultValue() {
-        // Without KAFKA_BOOTSTRAP_SERVERS env var set, should return "localhost:9092"
-        String envValue = System.getenv("KAFKA_BOOTSTRAP_SERVERS");
-        if (envValue == null) {
-            assertThat(config.getBootstrapServers()).isEqualTo("localhost:9092");
-        } else {
-            assertThat(config.getBootstrapServers()).isEqualTo(envValue);
-        }
+  @Test
+  void getBootstrapServers_defaultValue() {
+    // Without KAFKA_BOOTSTRAP_SERVERS env var set, should return "localhost:9092"
+    String envValue = System.getenv("KAFKA_BOOTSTRAP_SERVERS");
+    if (envValue == null) {
+      assertThat(config.getBootstrapServers()).isEqualTo("localhost:9092");
+    } else {
+      assertThat(config.getBootstrapServers()).isEqualTo(envValue);
     }
+  }
 
-    @Test
-    void getConsumerProperties_notNull() {
-        assertThat(config.getConsumerProperties()).isNotNull();
-    }
+  @Test
+  void getConsumerProperties_notNull() {
+    assertThat(config.getConsumerProperties()).isNotNull();
+  }
 
-    @Test
-    void getAdminProperties_notNull() {
-        assertThat(config.getAdminProperties()).isNotNull();
-    }
+  @Test
+  void getAdminProperties_notNull() {
+    assertThat(config.getAdminProperties()).isNotNull();
+  }
 
-    @Test
-    void getAvailableClusters_containsClusterName() {
-        assertThat(config.getAvailableClusters()).contains(config.getClusterName());
-        assertThat(config.getAvailableClusters()).hasSize(1);
-    }
+  @Test
+  void getAvailableClusters_containsClusterName() {
+    assertThat(config.getAvailableClusters()).contains(config.getClusterName());
+    assertThat(config.getAvailableClusters()).hasSize(1);
+  }
 
-    @Test
-    void forCluster_matchingName() {
-        ClusterConfig result = config.forCluster(config.getClusterName());
-        assertThat(result).isSameAs(config);
-    }
+  @Test
+  void forCluster_matchingName() {
+    ClusterConfig result = config.forCluster(config.getClusterName());
+    assertThat(result).isSameAs(config);
+  }
 
-    @Test
-    void forCluster_nonMatchingName() {
-        ClusterConfig result = config.forCluster("nonexistent-cluster");
-        assertThat(result).isNull();
-    }
+  @Test
+  void forCluster_nonMatchingName() {
+    ClusterConfig result = config.forCluster("nonexistent-cluster");
+    assertThat(result).isNull();
+  }
 
-    @Test
-    void getSecurityProperties_noProtocolSet() {
-        // If KAFKA_SECURITY_PROTOCOL is not set, should return empty map
-        String envValue = System.getenv("KAFKA_SECURITY_PROTOCOL");
-        if (envValue == null) {
-            assertThat(config.getSecurityProperties()).isEmpty();
-        } else {
-            assertThat(config.getSecurityProperties()).containsKey("security.protocol");
-            assertThat(config.getSecurityProperties().get("security.protocol")).isEqualTo(envValue);
-        }
+  @Test
+  void getSecurityProperties_noProtocolSet() {
+    // If KAFKA_SECURITY_PROTOCOL is not set, should return empty map
+    String envValue = System.getenv("KAFKA_SECURITY_PROTOCOL");
+    if (envValue == null) {
+      assertThat(config.getSecurityProperties()).isEmpty();
+    } else {
+      assertThat(config.getSecurityProperties()).containsKey("security.protocol");
+      assertThat(config.getSecurityProperties().get("security.protocol")).isEqualTo(envValue);
     }
+  }
 
-    @Test
-    void implementsClusterConfig() {
-        assertThat(config).isInstanceOf(ClusterConfig.class);
-    }
+  @Test
+  void implementsClusterConfig() {
+    assertThat(config).isInstanceOf(ClusterConfig.class);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/config/UGroupAutoConfigurationTest.java b/src/test/java/com/uber/ugroup/config/UGroupAutoConfigurationTest.java
index c849786..333d75b 100644
--- a/src/test/java/com/uber/ugroup/config/UGroupAutoConfigurationTest.java
+++ b/src/test/java/com/uber/ugroup/config/UGroupAutoConfigurationTest.java
@@ -15,6 +15,8 @@
  */
 package com.uber.ugroup.config;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import com.uber.ugroup.cache.CaffeineOffsetCache;
 import com.uber.ugroup.cache.InMemoryOffsetCache;
 import com.uber.ugroup.cache.NoopOffsetCache;
@@ -25,137 +27,134 @@
 import com.uber.ugroup.watchlist.BlockList;
 import com.uber.ugroup.watchlist.WatchListProvider;
 import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import java.util.List;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
 class UGroupAutoConfigurationTest {
 
-    private UGroupAutoConfiguration config;
-    private UGroupProperties properties;
+  private UGroupAutoConfiguration config;
+  private UGroupProperties properties;
 
-    @BeforeEach
-    void setUp() {
-        config = new UGroupAutoConfiguration();
-        properties = new UGroupProperties();
-    }
+  @BeforeEach
+  void setUp() {
+    config = new UGroupAutoConfiguration();
+    properties = new UGroupProperties();
+  }
 
-    @Test
-    void metricsProvider_createsMicrometerProvider() {
-        SimpleMeterRegistry registry = new SimpleMeterRegistry();
-        MetricsProvider provider = config.metricsProvider(registry);
+  @Test
+  void metricsProvider_createsMicrometerProvider() {
+    SimpleMeterRegistry registry = new SimpleMeterRegistry();
+    MetricsProvider provider = config.metricsProvider(registry);
 
-        assertThat(provider).isInstanceOf(MicrometerMetricsProvider.class);
-    }
+    assertThat(provider).isInstanceOf(MicrometerMetricsProvider.class);
+  }
 
-    @Test
-    void noopMetricsProvider_returnsNoopInstance() {
-        MetricsProvider provider = config.noopMetricsProvider();
+  @Test
+  void noopMetricsProvider_returnsNoopInstance() {
+    MetricsProvider provider = config.noopMetricsProvider();
 
-        assertThat(provider).isSameAs(NoopMetricsProvider.INSTANCE);
-    }
+    assertThat(provider).isSameAs(NoopMetricsProvider.INSTANCE);
+  }
 
-    @Test
-    void clusterConfig_createsSingleCluster() {
-        properties.getKafka().setClusterName("test-cluster");
-        properties.getKafka().setBootstrapServers("localhost:9092");
+  @Test
+  void clusterConfig_createsSingleCluster() {
+    properties.getKafka().setClusterName("test-cluster");
+    properties.getKafka().setBootstrapServers("localhost:9092");
 
-        ClusterConfig clusterConfig = config.clusterConfig(properties);
+    ClusterConfig clusterConfig = config.clusterConfig(properties);
 
-        assertThat(clusterConfig.getClusterName()).isEqualTo("test-cluster");
-        assertThat(clusterConfig.getBootstrapServers()).isEqualTo("localhost:9092");
-    }
+    assertThat(clusterConfig.getClusterName()).isEqualTo("test-cluster");
+    assertThat(clusterConfig.getBootstrapServers()).isEqualTo("localhost:9092");
+  }
 
-    @Test
-    void offsetCache_caffeine() {
-        properties.getCache().setType("caffeine");
-        properties.getCache().setMaxSize(500);
-        properties.getCache().setTtlSeconds(60);
+  @Test
+  void offsetCache_caffeine() {
+    properties.getCache().setType("caffeine");
+    properties.getCache().setMaxSize(500);
+    properties.getCache().setTtlSeconds(60);
 
-        OffsetCache cache = config.offsetCache(properties);
+    OffsetCache cache = config.offsetCache(properties);
 
-        assertThat(cache).isInstanceOf(CaffeineOffsetCache.class);
-    }
+    assertThat(cache).isInstanceOf(CaffeineOffsetCache.class);
+  }
 
-    @Test
-    void offsetCache_memory() {
-        properties.getCache().setType("memory");
+  @Test
+  void offsetCache_memory() {
+    properties.getCache().setType("memory");
 
-        OffsetCache cache = config.offsetCache(properties);
+    OffsetCache cache = config.offsetCache(properties);
 
-        assertThat(cache).isInstanceOf(InMemoryOffsetCache.class);
-    }
+    assertThat(cache).isInstanceOf(InMemoryOffsetCache.class);
+  }
 
-    @Test
-    void offsetCache_none() {
-        properties.getCache().setType("none");
+  @Test
+  void offsetCache_none() {
+    properties.getCache().setType("none");
 
-        OffsetCache cache = config.offsetCache(properties);
+    OffsetCache cache = config.offsetCache(properties);
 
-        assertThat(cache).isSameAs(NoopOffsetCache.INSTANCE);
-    }
+    assertThat(cache).isSameAs(NoopOffsetCache.INSTANCE);
+  }
 
-    @Test
-    void offsetCache_unknownType_defaultsToCaffeine() {
-        properties.getCache().setType("unknown");
+  @Test
+  void offsetCache_unknownType_defaultsToCaffeine() {
+    properties.getCache().setType("unknown");
 
-        OffsetCache cache = config.offsetCache(properties);
+    OffsetCache cache = config.offsetCache(properties);
 
-        assertThat(cache).isInstanceOf(CaffeineOffsetCache.class);
-    }
+    assertThat(cache).isInstanceOf(CaffeineOffsetCache.class);
+  }
 
-    @Test
-    void blockList_empty_whenNoFile() {
-        properties.getWatchlist().setBlocklistFile(null);
+  @Test
+  void blockList_empty_whenNoFile() {
+    properties.getWatchlist().setBlocklistFile(null);
 
-        BlockList blockList = config.blockList(properties);
+    BlockList blockList = config.blockList(properties);
 
-        assertThat(blockList.getBlockedGroups()).isEmpty();
-    }
+    assertThat(blockList.getBlockedGroups()).isEmpty();
+  }
 
-    @Test
-    void blockList_empty_whenEmptyString() {
-        properties.getWatchlist().setBlocklistFile("");
+  @Test
+  void blockList_empty_whenEmptyString() {
+    properties.getWatchlist().setBlocklistFile("");
 
-        BlockList blockList = config.blockList(properties);
+    BlockList blockList = config.blockList(properties);
 
-        assertThat(blockList.getBlockedGroups()).isEmpty();
-    }
+    assertThat(blockList.getBlockedGroups()).isEmpty();
+  }
 
-    @Test
-    void watchListProviders_allMode() {
-        properties.getWatchlist().setMode("all");
+  @Test
+  void watchListProviders_allMode() {
+    properties.getWatchlist().setMode("all");
 
-        List providers = config.watchListProviders(properties);
+    List providers = config.watchListProviders(properties);
 
-        assertThat(providers).hasSize(1);
-        assertThat(providers.get(0).getName()).isEqualTo("all");
-    }
+    assertThat(providers).hasSize(1);
+    assertThat(providers.get(0).getName()).isEqualTo("all");
+  }
 
-    @Test
-    void watchListProviders_regexMode() {
-        properties.getWatchlist().setMode("regex");
-        properties.getWatchlist().setIncludePatterns(List.of("prod-.*"));
+  @Test
+  void watchListProviders_regexMode() {
+    properties.getWatchlist().setMode("regex");
+    properties.getWatchlist().setIncludePatterns(List.of("prod-.*"));
 
-        List providers = config.watchListProviders(properties);
+    List providers = config.watchListProviders(properties);
 
-        assertThat(providers).hasSize(2); // regex + all (fallback)
-        assertThat(providers.get(0).getName()).isEqualTo("regex");
-        assertThat(providers.get(1).getName()).isEqualTo("all");
-    }
+    assertThat(providers).hasSize(2); // regex + all (fallback)
+    assertThat(providers.get(0).getName()).isEqualTo("regex");
+    assertThat(providers.get(1).getName()).isEqualTo("all");
+  }
 
-    @Test
-    void watchListProviders_staticMode_noFile_onlyAll() {
-        properties.getWatchlist().setMode("static");
-        properties.getWatchlist().setStaticFile(null);
+  @Test
+  void watchListProviders_staticMode_noFile_onlyAll() {
+    properties.getWatchlist().setMode("static");
+    properties.getWatchlist().setStaticFile(null);
 
-        List providers = config.watchListProviders(properties);
+    List providers = config.watchListProviders(properties);
 
-        // Only AllConsumersWatchListProvider since no static file
-        assertThat(providers).hasSize(1);
-        assertThat(providers.get(0).getName()).isEqualTo("all");
-    }
+    // Only AllConsumersWatchListProvider since no static file
+    assertThat(providers).hasSize(1);
+    assertThat(providers.get(0).getName()).isEqualTo("all");
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/config/UGroupPropertiesTest.java b/src/test/java/com/uber/ugroup/config/UGroupPropertiesTest.java
index cc74ef3..934de7e 100644
--- a/src/test/java/com/uber/ugroup/config/UGroupPropertiesTest.java
+++ b/src/test/java/com/uber/ugroup/config/UGroupPropertiesTest.java
@@ -15,264 +15,264 @@
  */
 package com.uber.ugroup.config;
 
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 class UGroupPropertiesTest {
 
-    private UGroupProperties properties;
-
-    @BeforeEach
-    void setUp() {
-        properties = new UGroupProperties();
-    }
-
-    // --- Top-level getters ---
-
-    @Test
-    void nestedObjects_areNotNull() {
-        assertThat(properties.getKafka()).isNotNull();
-        assertThat(properties.getProcessing()).isNotNull();
-        assertThat(properties.getWatchlist()).isNotNull();
-        assertThat(properties.getMetrics()).isNotNull();
-        assertThat(properties.getCache()).isNotNull();
-    }
-
-    // --- Kafka defaults ---
-
-    @Test
-    void kafka_defaultBootstrapServers() {
-        assertThat(properties.getKafka().getBootstrapServers()).isEqualTo("localhost:9092");
-    }
-
-    @Test
-    void kafka_defaultConsumerGroup() {
-        assertThat(properties.getKafka().getConsumerGroup()).isEqualTo("ugroup-monitor");
-    }
-
-    @Test
-    void kafka_defaultClusterName() {
-        assertThat(properties.getKafka().getClusterName()).isEqualTo("default");
-    }
-
-    @Test
-    void kafka_defaultSecurityProtocol() {
-        assertThat(properties.getKafka().getSecurityProtocol()).isEqualTo("PLAINTEXT");
-    }
-
-    // --- Kafka setters ---
-
-    @Test
-    void kafka_setBootstrapServers() {
-        properties.getKafka().setBootstrapServers("broker1:9092,broker2:9092");
-        assertThat(properties.getKafka().getBootstrapServers()).isEqualTo("broker1:9092,broker2:9092");
-    }
-
-    @Test
-    void kafka_setConsumerGroup() {
-        properties.getKafka().setConsumerGroup("my-group");
-        assertThat(properties.getKafka().getConsumerGroup()).isEqualTo("my-group");
-    }
-
-    @Test
-    void kafka_setClusterName() {
-        properties.getKafka().setClusterName("production");
-        assertThat(properties.getKafka().getClusterName()).isEqualTo("production");
-    }
-
-    @Test
-    void kafka_setSecurityProtocol() {
-        properties.getKafka().setSecurityProtocol("SSL");
-        assertThat(properties.getKafka().getSecurityProtocol()).isEqualTo("SSL");
-    }
-
-    // --- Processing defaults ---
-
-    @Test
-    void processing_defaultThreadCount() {
-        assertThat(properties.getProcessing().getThreadCount()).isEqualTo(1);
-    }
-
-    @Test
-    void processing_defaultParallelism() {
-        assertThat(properties.getProcessing().getParallelism()).isEqualTo(8);
-    }
-
-    @Test
-    void processing_defaultCompactionIntervalMs() {
-        assertThat(properties.getProcessing().getCompactionIntervalMs()).isEqualTo(5000);
-    }
-
-    @Test
-    void processing_defaultLagReportIntervalMs() {
-        assertThat(properties.getProcessing().getLagReportIntervalMs()).isEqualTo(10000);
-    }
-
-    @Test
-    void processing_defaultConsumerOffsetsPartitionCount() {
-        assertThat(properties.getProcessing().getConsumerOffsetsPartitionCount()).isEqualTo(50);
-    }
-
-    @Test
-    void processing_defaultStuckPartitionThresholdMs() {
-        assertThat(properties.getProcessing().getStuckPartitionThresholdMs()).isEqualTo(300000);
-    }
-
-    // --- Processing setters ---
-
-    @Test
-    void processing_setThreadCount() {
-        properties.getProcessing().setThreadCount(4);
-        assertThat(properties.getProcessing().getThreadCount()).isEqualTo(4);
-    }
-
-    @Test
-    void processing_setParallelism() {
-        properties.getProcessing().setParallelism(16);
-        assertThat(properties.getProcessing().getParallelism()).isEqualTo(16);
-    }
-
-    @Test
-    void processing_setCompactionIntervalMs() {
-        properties.getProcessing().setCompactionIntervalMs(10000);
-        assertThat(properties.getProcessing().getCompactionIntervalMs()).isEqualTo(10000);
-    }
-
-    @Test
-    void processing_setLagReportIntervalMs() {
-        properties.getProcessing().setLagReportIntervalMs(30000);
-        assertThat(properties.getProcessing().getLagReportIntervalMs()).isEqualTo(30000);
-    }
-
-    @Test
-    void processing_setConsumerOffsetsPartitionCount() {
-        properties.getProcessing().setConsumerOffsetsPartitionCount(100);
-        assertThat(properties.getProcessing().getConsumerOffsetsPartitionCount()).isEqualTo(100);
-    }
-
-    @Test
-    void processing_setStuckPartitionThresholdMs() {
-        properties.getProcessing().setStuckPartitionThresholdMs(600000);
-        assertThat(properties.getProcessing().getStuckPartitionThresholdMs()).isEqualTo(600000);
-    }
-
-    // --- Watchlist defaults ---
-
-    @Test
-    void watchlist_defaultMode() {
-        assertThat(properties.getWatchlist().getMode()).isEqualTo("all");
-    }
-
-    @Test
-    void watchlist_defaultStaticFile() {
-        assertThat(properties.getWatchlist().getStaticFile()).isNull();
-    }
-
-    @Test
-    void watchlist_defaultBlocklistFile() {
-        assertThat(properties.getWatchlist().getBlocklistFile()).isNull();
-    }
-
-    @Test
-    void watchlist_defaultIncludePatterns() {
-        assertThat(properties.getWatchlist().getIncludePatterns()).isEmpty();
-    }
-
-    @Test
-    void watchlist_defaultExcludePatterns() {
-        assertThat(properties.getWatchlist().getExcludePatterns()).isEmpty();
-    }
-
-    // --- Watchlist setters ---
-
-    @Test
-    void watchlist_setMode() {
-        properties.getWatchlist().setMode("regex");
-        assertThat(properties.getWatchlist().getMode()).isEqualTo("regex");
-    }
-
-    @Test
-    void watchlist_setStaticFile() {
-        properties.getWatchlist().setStaticFile("/etc/watchlist.json");
-        assertThat(properties.getWatchlist().getStaticFile()).isEqualTo("/etc/watchlist.json");
-    }
-
-    @Test
-    void watchlist_setBlocklistFile() {
-        properties.getWatchlist().setBlocklistFile("/etc/blocklist.json");
-        assertThat(properties.getWatchlist().getBlocklistFile()).isEqualTo("/etc/blocklist.json");
-    }
-
-    @Test
-    void watchlist_setIncludePatterns() {
-        properties.getWatchlist().setIncludePatterns(List.of("group-.*", "test-.*"));
-        assertThat(properties.getWatchlist().getIncludePatterns()).containsExactly("group-.*", "test-.*");
-    }
-
-    @Test
-    void watchlist_setExcludePatterns() {
-        properties.getWatchlist().setExcludePatterns(List.of("internal-.*"));
-        assertThat(properties.getWatchlist().getExcludePatterns()).containsExactly("internal-.*");
-    }
-
-    // --- Metrics defaults and setters ---
-
-    @Test
-    void metrics_defaultEnabled() {
-        assertThat(properties.getMetrics().isEnabled()).isTrue();
-    }
-
-    @Test
-    void metrics_setEnabled_false() {
-        properties.getMetrics().setEnabled(false);
-        assertThat(properties.getMetrics().isEnabled()).isFalse();
-    }
-
-    @Test
-    void metrics_setEnabled_true() {
-        properties.getMetrics().setEnabled(false);
-        properties.getMetrics().setEnabled(true);
-        assertThat(properties.getMetrics().isEnabled()).isTrue();
-    }
-
-    // --- Cache defaults ---
-
-    @Test
-    void cache_defaultType() {
-        assertThat(properties.getCache().getType()).isEqualTo("caffeine");
-    }
-
-    @Test
-    void cache_defaultMaxSize() {
-        assertThat(properties.getCache().getMaxSize()).isEqualTo(10000);
-    }
-
-    @Test
-    void cache_defaultTtlSeconds() {
-        assertThat(properties.getCache().getTtlSeconds()).isEqualTo(300);
-    }
-
-    // --- Cache setters ---
-
-    @Test
-    void cache_setType() {
-        properties.getCache().setType("memory");
-        assertThat(properties.getCache().getType()).isEqualTo("memory");
-    }
-
-    @Test
-    void cache_setMaxSize() {
-        properties.getCache().setMaxSize(50000);
-        assertThat(properties.getCache().getMaxSize()).isEqualTo(50000);
-    }
-
-    @Test
-    void cache_setTtlSeconds() {
-        properties.getCache().setTtlSeconds(600);
-        assertThat(properties.getCache().getTtlSeconds()).isEqualTo(600);
-    }
+  private UGroupProperties properties;
+
+  @BeforeEach
+  void setUp() {
+    properties = new UGroupProperties();
+  }
+
+  // --- Top-level getters ---
+
+  @Test
+  void nestedObjects_areNotNull() {
+    assertThat(properties.getKafka()).isNotNull();
+    assertThat(properties.getProcessing()).isNotNull();
+    assertThat(properties.getWatchlist()).isNotNull();
+    assertThat(properties.getMetrics()).isNotNull();
+    assertThat(properties.getCache()).isNotNull();
+  }
+
+  // --- Kafka defaults ---
+
+  @Test
+  void kafka_defaultBootstrapServers() {
+    assertThat(properties.getKafka().getBootstrapServers()).isEqualTo("localhost:9092");
+  }
+
+  @Test
+  void kafka_defaultConsumerGroup() {
+    assertThat(properties.getKafka().getConsumerGroup()).isEqualTo("ugroup-monitor");
+  }
+
+  @Test
+  void kafka_defaultClusterName() {
+    assertThat(properties.getKafka().getClusterName()).isEqualTo("default");
+  }
+
+  @Test
+  void kafka_defaultSecurityProtocol() {
+    assertThat(properties.getKafka().getSecurityProtocol()).isEqualTo("PLAINTEXT");
+  }
+
+  // --- Kafka setters ---
+
+  @Test
+  void kafka_setBootstrapServers() {
+    properties.getKafka().setBootstrapServers("broker1:9092,broker2:9092");
+    assertThat(properties.getKafka().getBootstrapServers()).isEqualTo("broker1:9092,broker2:9092");
+  }
+
+  @Test
+  void kafka_setConsumerGroup() {
+    properties.getKafka().setConsumerGroup("my-group");
+    assertThat(properties.getKafka().getConsumerGroup()).isEqualTo("my-group");
+  }
+
+  @Test
+  void kafka_setClusterName() {
+    properties.getKafka().setClusterName("production");
+    assertThat(properties.getKafka().getClusterName()).isEqualTo("production");
+  }
+
+  @Test
+  void kafka_setSecurityProtocol() {
+    properties.getKafka().setSecurityProtocol("SSL");
+    assertThat(properties.getKafka().getSecurityProtocol()).isEqualTo("SSL");
+  }
+
+  // --- Processing defaults ---
+
+  @Test
+  void processing_defaultThreadCount() {
+    assertThat(properties.getProcessing().getThreadCount()).isEqualTo(1);
+  }
+
+  @Test
+  void processing_defaultParallelism() {
+    assertThat(properties.getProcessing().getParallelism()).isEqualTo(8);
+  }
+
+  @Test
+  void processing_defaultCompactionIntervalMs() {
+    assertThat(properties.getProcessing().getCompactionIntervalMs()).isEqualTo(5000);
+  }
+
+  @Test
+  void processing_defaultLagReportIntervalMs() {
+    assertThat(properties.getProcessing().getLagReportIntervalMs()).isEqualTo(10000);
+  }
+
+  @Test
+  void processing_defaultConsumerOffsetsPartitionCount() {
+    assertThat(properties.getProcessing().getConsumerOffsetsPartitionCount()).isEqualTo(50);
+  }
+
+  @Test
+  void processing_defaultStuckPartitionThresholdMs() {
+    assertThat(properties.getProcessing().getStuckPartitionThresholdMs()).isEqualTo(300000);
+  }
+
+  // --- Processing setters ---
+
+  @Test
+  void processing_setThreadCount() {
+    properties.getProcessing().setThreadCount(4);
+    assertThat(properties.getProcessing().getThreadCount()).isEqualTo(4);
+  }
+
+  @Test
+  void processing_setParallelism() {
+    properties.getProcessing().setParallelism(16);
+    assertThat(properties.getProcessing().getParallelism()).isEqualTo(16);
+  }
+
+  @Test
+  void processing_setCompactionIntervalMs() {
+    properties.getProcessing().setCompactionIntervalMs(10000);
+    assertThat(properties.getProcessing().getCompactionIntervalMs()).isEqualTo(10000);
+  }
+
+  @Test
+  void processing_setLagReportIntervalMs() {
+    properties.getProcessing().setLagReportIntervalMs(30000);
+    assertThat(properties.getProcessing().getLagReportIntervalMs()).isEqualTo(30000);
+  }
+
+  @Test
+  void processing_setConsumerOffsetsPartitionCount() {
+    properties.getProcessing().setConsumerOffsetsPartitionCount(100);
+    assertThat(properties.getProcessing().getConsumerOffsetsPartitionCount()).isEqualTo(100);
+  }
+
+  @Test
+  void processing_setStuckPartitionThresholdMs() {
+    properties.getProcessing().setStuckPartitionThresholdMs(600000);
+    assertThat(properties.getProcessing().getStuckPartitionThresholdMs()).isEqualTo(600000);
+  }
+
+  // --- Watchlist defaults ---
+
+  @Test
+  void watchlist_defaultMode() {
+    assertThat(properties.getWatchlist().getMode()).isEqualTo("all");
+  }
+
+  @Test
+  void watchlist_defaultStaticFile() {
+    assertThat(properties.getWatchlist().getStaticFile()).isNull();
+  }
+
+  @Test
+  void watchlist_defaultBlocklistFile() {
+    assertThat(properties.getWatchlist().getBlocklistFile()).isNull();
+  }
+
+  @Test
+  void watchlist_defaultIncludePatterns() {
+    assertThat(properties.getWatchlist().getIncludePatterns()).isEmpty();
+  }
+
+  @Test
+  void watchlist_defaultExcludePatterns() {
+    assertThat(properties.getWatchlist().getExcludePatterns()).isEmpty();
+  }
+
+  // --- Watchlist setters ---
+
+  @Test
+  void watchlist_setMode() {
+    properties.getWatchlist().setMode("regex");
+    assertThat(properties.getWatchlist().getMode()).isEqualTo("regex");
+  }
+
+  @Test
+  void watchlist_setStaticFile() {
+    properties.getWatchlist().setStaticFile("/etc/watchlist.json");
+    assertThat(properties.getWatchlist().getStaticFile()).isEqualTo("/etc/watchlist.json");
+  }
+
+  @Test
+  void watchlist_setBlocklistFile() {
+    properties.getWatchlist().setBlocklistFile("/etc/blocklist.json");
+    assertThat(properties.getWatchlist().getBlocklistFile()).isEqualTo("/etc/blocklist.json");
+  }
+
+  @Test
+  void watchlist_setIncludePatterns() {
+    properties.getWatchlist().setIncludePatterns(List.of("group-.*", "test-.*"));
+    assertThat(properties.getWatchlist().getIncludePatterns())
+        .containsExactly("group-.*", "test-.*");
+  }
+
+  @Test
+  void watchlist_setExcludePatterns() {
+    properties.getWatchlist().setExcludePatterns(List.of("internal-.*"));
+    assertThat(properties.getWatchlist().getExcludePatterns()).containsExactly("internal-.*");
+  }
+
+  // --- Metrics defaults and setters ---
+
+  @Test
+  void metrics_defaultEnabled() {
+    assertThat(properties.getMetrics().isEnabled()).isTrue();
+  }
+
+  @Test
+  void metrics_setEnabled_false() {
+    properties.getMetrics().setEnabled(false);
+    assertThat(properties.getMetrics().isEnabled()).isFalse();
+  }
+
+  @Test
+  void metrics_setEnabled_true() {
+    properties.getMetrics().setEnabled(false);
+    properties.getMetrics().setEnabled(true);
+    assertThat(properties.getMetrics().isEnabled()).isTrue();
+  }
+
+  // --- Cache defaults ---
+
+  @Test
+  void cache_defaultType() {
+    assertThat(properties.getCache().getType()).isEqualTo("caffeine");
+  }
+
+  @Test
+  void cache_defaultMaxSize() {
+    assertThat(properties.getCache().getMaxSize()).isEqualTo(10000);
+  }
+
+  @Test
+  void cache_defaultTtlSeconds() {
+    assertThat(properties.getCache().getTtlSeconds()).isEqualTo(300);
+  }
+
+  // --- Cache setters ---
+
+  @Test
+  void cache_setType() {
+    properties.getCache().setType("memory");
+    assertThat(properties.getCache().getType()).isEqualTo("memory");
+  }
+
+  @Test
+  void cache_setMaxSize() {
+    properties.getCache().setMaxSize(50000);
+    assertThat(properties.getCache().getMaxSize()).isEqualTo(50000);
+  }
+
+  @Test
+  void cache_setTtlSeconds() {
+    properties.getCache().setTtlSeconds(600);
+    assertThat(properties.getCache().getTtlSeconds()).isEqualTo(600);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/config/YamlClusterConfigTest.java b/src/test/java/com/uber/ugroup/config/YamlClusterConfigTest.java
index 6483bb3..56d3922 100644
--- a/src/test/java/com/uber/ugroup/config/YamlClusterConfigTest.java
+++ b/src/test/java/com/uber/ugroup/config/YamlClusterConfigTest.java
@@ -15,78 +15,80 @@
  */
 package com.uber.ugroup.config;
 
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Properties;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 class YamlClusterConfigTest {
 
-    @Test
-    void single_clusterName() {
-        YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
+  @Test
+  void single_clusterName() {
+    YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
 
-        assertThat(config.getClusterName()).isEqualTo("my-cluster");
-    }
+    assertThat(config.getClusterName()).isEqualTo("my-cluster");
+  }
 
-    @Test
-    void single_bootstrapServers() {
-        YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092,broker2:9092");
+  @Test
+  void single_bootstrapServers() {
+    YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092,broker2:9092");
 
-        assertThat(config.getBootstrapServers()).isEqualTo("broker1:9092,broker2:9092");
-    }
+    assertThat(config.getBootstrapServers()).isEqualTo("broker1:9092,broker2:9092");
+  }
 
-    @Test
-    void single_emptyProperties() {
-        YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
+  @Test
+  void single_emptyProperties() {
+    YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
 
-        assertThat(config.getConsumerProperties()).isEmpty();
-        assertThat(config.getAdminProperties()).isEmpty();
-    }
+    assertThat(config.getConsumerProperties()).isEmpty();
+    assertThat(config.getAdminProperties()).isEmpty();
+  }
 
-    @Test
-    void single_withProperties() {
-        Properties props = new Properties();
-        props.setProperty("security.protocol", "SSL");
-        props.setProperty("ssl.truststore.location", "/path/to/truststore");
+  @Test
+  void single_withProperties() {
+    Properties props = new Properties();
+    props.setProperty("security.protocol", "SSL");
+    props.setProperty("ssl.truststore.location", "/path/to/truststore");
 
-        YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092", props);
+    YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092", props);
 
-        assertThat(config.getConsumerProperties()).containsEntry("security.protocol", "SSL");
-        assertThat(config.getConsumerProperties()).containsEntry("ssl.truststore.location", "/path/to/truststore");
-    }
+    assertThat(config.getConsumerProperties()).containsEntry("security.protocol", "SSL");
+    assertThat(config.getConsumerProperties())
+        .containsEntry("ssl.truststore.location", "/path/to/truststore");
+  }
 
-    @Test
-    void single_availableClusters() {
-        YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
+  @Test
+  void single_availableClusters() {
+    YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
 
-        assertThat(config.getAvailableClusters()).containsExactly("my-cluster");
-    }
+    assertThat(config.getAvailableClusters()).containsExactly("my-cluster");
+  }
 
-    @Test
-    void single_forCluster_matching() {
-        YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
+  @Test
+  void single_forCluster_matching() {
+    YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
 
-        assertThat(config.forCluster("my-cluster")).isSameAs(config);
-    }
+    assertThat(config.forCluster("my-cluster")).isSameAs(config);
+  }
 
-    @Test
-    void single_forCluster_nonMatching() {
-        YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
+  @Test
+  void single_forCluster_nonMatching() {
+    YamlClusterConfig config = YamlClusterConfig.single("my-cluster", "broker1:9092");
 
-        assertThat(config.forCluster("other-cluster")).isNull();
-    }
+    assertThat(config.forCluster("other-cluster")).isNull();
+  }
 
-    @Test
-    void fromYaml_singleCluster(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("clusters.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void fromYaml_singleCluster(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("clusters.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 clusters:
                   test-cluster:
                     bootstrap-servers: "broker1:9092,broker2:9092"
@@ -94,17 +96,19 @@ void fromYaml_singleCluster(@TempDir Path tempDir) throws IOException {
                       security.protocol: PLAINTEXT
                 """);
 
-        YamlClusterConfig config = new YamlClusterConfig(yamlFile.toString(), "test-cluster");
+    YamlClusterConfig config = new YamlClusterConfig(yamlFile.toString(), "test-cluster");
 
-        assertThat(config.getClusterName()).isEqualTo("test-cluster");
-        assertThat(config.getBootstrapServers()).isEqualTo("broker1:9092,broker2:9092");
-        assertThat(config.getConsumerProperties()).containsEntry("security.protocol", "PLAINTEXT");
-    }
+    assertThat(config.getClusterName()).isEqualTo("test-cluster");
+    assertThat(config.getBootstrapServers()).isEqualTo("broker1:9092,broker2:9092");
+    assertThat(config.getConsumerProperties()).containsEntry("security.protocol", "PLAINTEXT");
+  }
 
-    @Test
-    void fromYaml_multipleClusters(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("clusters.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void fromYaml_multipleClusters(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("clusters.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 clusters:
                   cluster-a:
                     bootstrap-servers: "broker-a:9092"
@@ -116,17 +120,19 @@ void fromYaml_multipleClusters(@TempDir Path tempDir) throws IOException {
                       security.protocol: SSL
                 """);
 
-        YamlClusterConfig config = new YamlClusterConfig(yamlFile.toString(), "cluster-a");
+    YamlClusterConfig config = new YamlClusterConfig(yamlFile.toString(), "cluster-a");
 
-        assertThat(config.getClusterName()).isEqualTo("cluster-a");
-        assertThat(config.getBootstrapServers()).isEqualTo("broker-a:9092");
-        assertThat(config.getAvailableClusters()).containsExactlyInAnyOrder("cluster-a", "cluster-b");
-    }
+    assertThat(config.getClusterName()).isEqualTo("cluster-a");
+    assertThat(config.getBootstrapServers()).isEqualTo("broker-a:9092");
+    assertThat(config.getAvailableClusters()).containsExactlyInAnyOrder("cluster-a", "cluster-b");
+  }
 
-    @Test
-    void fromYaml_forCluster_differentCluster(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("clusters.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void fromYaml_forCluster_differentCluster(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("clusters.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 clusters:
                   cluster-a:
                     bootstrap-servers: "broker-a:9092"
@@ -136,56 +142,60 @@ void fromYaml_forCluster_differentCluster(@TempDir Path tempDir) throws IOExcept
                     properties: {}
                 """);
 
-        YamlClusterConfig config = new YamlClusterConfig(yamlFile.toString(), "cluster-a");
+    YamlClusterConfig config = new YamlClusterConfig(yamlFile.toString(), "cluster-a");
 
-        ClusterConfig clusterB = config.forCluster("cluster-b");
-        assertThat(clusterB).isNotNull();
-        assertThat(clusterB.getClusterName()).isEqualTo("cluster-b");
-        assertThat(clusterB.getBootstrapServers()).isEqualTo("broker-b:9092");
-    }
+    ClusterConfig clusterB = config.forCluster("cluster-b");
+    assertThat(clusterB).isNotNull();
+    assertThat(clusterB.getClusterName()).isEqualTo("cluster-b");
+    assertThat(clusterB.getBootstrapServers()).isEqualTo("broker-b:9092");
+  }
 
-    @Test
-    void fromYaml_clusterNotFound(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("clusters.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void fromYaml_clusterNotFound(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("clusters.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 clusters:
                   existing-cluster:
                     bootstrap-servers: "broker:9092"
                     properties: {}
                 """);
 
-        assertThatThrownBy(() -> new YamlClusterConfig(yamlFile.toString(), "missing-cluster"))
-                .isInstanceOf(IllegalArgumentException.class)
-                .hasMessageContaining("missing-cluster")
-                .hasMessageContaining("not found");
-    }
-
-    @Test
-    void fromYaml_fileNotFound() {
-        assertThatThrownBy(() -> new YamlClusterConfig("/nonexistent/path.yaml", "cluster"))
-                .isInstanceOf(RuntimeException.class)
-                .hasMessageContaining("Failed to load cluster config");
-    }
-
-    @Test
-    void fromYaml_noProperties(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("clusters.yaml");
-        Files.writeString(yamlFile, """
+    assertThatThrownBy(() -> new YamlClusterConfig(yamlFile.toString(), "missing-cluster"))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining("missing-cluster")
+        .hasMessageContaining("not found");
+  }
+
+  @Test
+  void fromYaml_fileNotFound() {
+    assertThatThrownBy(() -> new YamlClusterConfig("/nonexistent/path.yaml", "cluster"))
+        .isInstanceOf(RuntimeException.class)
+        .hasMessageContaining("Failed to load cluster config");
+  }
+
+  @Test
+  void fromYaml_noProperties(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("clusters.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 clusters:
                   minimal-cluster:
                     bootstrap-servers: "broker:9092"
                 """);
 
-        YamlClusterConfig config = new YamlClusterConfig(yamlFile.toString(), "minimal-cluster");
+    YamlClusterConfig config = new YamlClusterConfig(yamlFile.toString(), "minimal-cluster");
 
-        assertThat(config.getClusterName()).isEqualTo("minimal-cluster");
-        assertThat(config.getBootstrapServers()).isEqualTo("broker:9092");
-        assertThat(config.getConsumerProperties()).isEmpty();
-    }
+    assertThat(config.getClusterName()).isEqualTo("minimal-cluster");
+    assertThat(config.getBootstrapServers()).isEqualTo("broker:9092");
+    assertThat(config.getConsumerProperties()).isEmpty();
+  }
 
-    @Test
-    void implementsClusterConfig() {
-        YamlClusterConfig config = YamlClusterConfig.single("test", "localhost:9092");
-        assertThat(config).isInstanceOf(ClusterConfig.class);
-    }
+  @Test
+  void implementsClusterConfig() {
+    YamlClusterConfig config = YamlClusterConfig.single("test", "localhost:9092");
+    assertThat(config).isInstanceOf(ClusterConfig.class);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/fetcher/DirectOffsetFetcherTest.java b/src/test/java/com/uber/ugroup/fetcher/DirectOffsetFetcherTest.java
index 775aac9..30ab1f2 100644
--- a/src/test/java/com/uber/ugroup/fetcher/DirectOffsetFetcherTest.java
+++ b/src/test/java/com/uber/ugroup/fetcher/DirectOffsetFetcherTest.java
@@ -15,6 +15,21 @@
  */
 package com.uber.ugroup.fetcher;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import org.apache.kafka.clients.admin.AdminClient;
 import org.apache.kafka.clients.admin.ConsumerGroupDescription;
 import org.apache.kafka.clients.admin.DescribeConsumerGroupsResult;
@@ -35,308 +50,321 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
-import java.lang.reflect.Field;
-import java.time.Duration;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @ExtendWith(MockitoExtension.class)
 class DirectOffsetFetcherTest {
 
-    @Mock
-    private AdminClient adminClient;
-
-    @Mock
-    private KafkaConsumer consumer;
-
-    private DirectOffsetFetcher fetcher;
-
-    @BeforeEach
-    void setUp() throws Exception {
-        // Create a DirectOffsetFetcher and inject mocks via reflection
-        // since the constructor creates real clients
-        fetcher = createFetcherWithMocks();
-    }
-
-    @Test
-    void beginningOffsets_returnsOffsets() {
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        TopicPartition tp1 = new TopicPartition("topic", 1);
-        List partitions = List.of(tp0, tp1);
-
-        when(consumer.beginningOffsets(eq(partitions), any(Duration.class)))
-                .thenReturn(Map.of(tp0, 0L, tp1, 100L));
-
-        Map result = fetcher.beginningOffsets(partitions);
+  @Mock private AdminClient adminClient;
 
-        assertThat(result).hasSize(2);
-        assertThat(result.get(tp0)).isEqualTo(0L);
-        assertThat(result.get(tp1)).isEqualTo(100L);
-    }
+  @Mock private KafkaConsumer consumer;
 
-    @Test
-    void beginningOffsets_onException_returnsEmptyMap() {
-        List partitions = List.of(new TopicPartition("topic", 0));
+  private DirectOffsetFetcher fetcher;
 
-        when(consumer.beginningOffsets(any(Collection.class), any(Duration.class)))
-                .thenThrow(new RuntimeException("Connection failed"));
+  @BeforeEach
+  void setUp() throws Exception {
+    // Create a DirectOffsetFetcher and inject mocks via reflection
+    // since the constructor creates real clients
+    fetcher = createFetcherWithMocks();
+  }
 
-        Map result = fetcher.beginningOffsets(partitions);
+  @Test
+  void beginningOffsets_returnsOffsets() {
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    TopicPartition tp1 = new TopicPartition("topic", 1);
+    List partitions = List.of(tp0, tp1);
 
-        assertThat(result).isEmpty();
-    }
+    when(consumer.beginningOffsets(eq(partitions), any(Duration.class)))
+        .thenReturn(Map.of(tp0, 0L, tp1, 100L));
 
-    @Test
-    void endOffsets_returnsOffsets() {
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        List partitions = List.of(tp0);
+    Map result = fetcher.beginningOffsets(partitions);
 
-        when(consumer.endOffsets(eq(partitions), any(Duration.class)))
-                .thenReturn(Map.of(tp0, 500L));
+    assertThat(result).hasSize(2);
+    assertThat(result.get(tp0)).isEqualTo(0L);
+    assertThat(result.get(tp1)).isEqualTo(100L);
+  }
 
-        Map result = fetcher.endOffsets(partitions);
+  @Test
+  void beginningOffsets_onException_returnsEmptyMap() {
+    List partitions = List.of(new TopicPartition("topic", 0));
 
-        assertThat(result).hasSize(1);
-        assertThat(result.get(tp0)).isEqualTo(500L);
-    }
+    when(consumer.beginningOffsets(any(Collection.class), any(Duration.class)))
+        .thenThrow(new RuntimeException("Connection failed"));
 
-    @Test
-    void endOffsets_onException_returnsEmptyMap() {
-        when(consumer.endOffsets(any(Collection.class), any(Duration.class)))
-                .thenThrow(new RuntimeException("Timeout"));
+    Map result = fetcher.beginningOffsets(partitions);
 
-        Map result = fetcher.endOffsets(List.of(new TopicPartition("t", 0)));
+    assertThat(result).isEmpty();
+  }
 
-        assertThat(result).isEmpty();
-    }
+  @Test
+  void endOffsets_returnsOffsets() {
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    List partitions = List.of(tp0);
 
-    @Test
-    void partitionsFor_returnsPartitionInfo() {
-        Node node = new Node(0, "localhost", 9092);
-        List expected = List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node}),
-                new PartitionInfo("topic", 1, node, new Node[]{node}, new Node[]{node})
-        );
+    when(consumer.endOffsets(eq(partitions), any(Duration.class))).thenReturn(Map.of(tp0, 500L));
 
-        when(consumer.partitionsFor(eq("topic"), any(Duration.class))).thenReturn(expected);
+    Map result = fetcher.endOffsets(partitions);
 
-        List result = fetcher.partitionsFor("topic");
+    assertThat(result).hasSize(1);
+    assertThat(result.get(tp0)).isEqualTo(500L);
+  }
 
-        assertThat(result).hasSize(2);
-    }
+  @Test
+  void endOffsets_onException_returnsEmptyMap() {
+    when(consumer.endOffsets(any(Collection.class), any(Duration.class)))
+        .thenThrow(new RuntimeException("Timeout"));
 
-    @Test
-    void partitionsFor_onException_returnsNull() {
-        when(consumer.partitionsFor(any(String.class), any(Duration.class)))
-                .thenThrow(new RuntimeException("Error"));
+    Map result = fetcher.endOffsets(List.of(new TopicPartition("t", 0)));
 
-        List result = fetcher.partitionsFor("topic");
+    assertThat(result).isEmpty();
+  }
 
-        assertThat(result).isNull();
-    }
+  @Test
+  void partitionsFor_returnsPartitionInfo() {
+    Node node = new Node(0, "localhost", 9092);
+    List expected =
+        List.of(
+            new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node}),
+            new PartitionInfo("topic", 1, node, new Node[] {node}, new Node[] {node}));
 
-    @Test
-    void partitionCount_returnsSize() {
-        Node node = new Node(0, "localhost", 9092);
-        List partitions = List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node}),
-                new PartitionInfo("topic", 1, node, new Node[]{node}, new Node[]{node}),
-                new PartitionInfo("topic", 2, node, new Node[]{node}, new Node[]{node})
-        );
+    when(consumer.partitionsFor(eq("topic"), any(Duration.class))).thenReturn(expected);
 
-        when(consumer.partitionsFor(eq("topic"), any(Duration.class))).thenReturn(partitions);
+    List result = fetcher.partitionsFor("topic");
 
-        assertThat(fetcher.partitionCount("topic")).isEqualTo(3);
-    }
+    assertThat(result).hasSize(2);
+  }
 
-    @Test
-    void partitionCount_nullPartitions_returnsZero() {
-        when(consumer.partitionsFor(any(String.class), any(Duration.class)))
-                .thenThrow(new RuntimeException("Error"));
+  @Test
+  void partitionsFor_onException_returnsNull() {
+    when(consumer.partitionsFor(any(String.class), any(Duration.class)))
+        .thenThrow(new RuntimeException("Error"));
 
-        assertThat(fetcher.partitionCount("topic")).isEqualTo(0);
-    }
+    List result = fetcher.partitionsFor("topic");
 
-    @Test
-    void listConsumerGroupOffsets_returnsOffsets() throws Exception {
-        TopicPartition tp = new TopicPartition("topic", 0);
-        Map expected = Map.of(tp, new OffsetAndMetadata(42L));
+    assertThat(result).isNull();
+  }
 
-        ListConsumerGroupOffsetsResult result = mockListConsumerGroupOffsetsResult(expected);
-        when(adminClient.listConsumerGroupOffsets("my-group")).thenReturn(result);
+  @Test
+  void partitionCount_returnsSize() {
+    Node node = new Node(0, "localhost", 9092);
+    List partitions =
+        List.of(
+            new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node}),
+            new PartitionInfo("topic", 1, node, new Node[] {node}, new Node[] {node}),
+            new PartitionInfo("topic", 2, node, new Node[] {node}, new Node[] {node}));
 
-        Map actual = fetcher.listConsumerGroupOffsets("my-group");
+    when(consumer.partitionsFor(eq("topic"), any(Duration.class))).thenReturn(partitions);
 
-        assertThat(actual).hasSize(1);
-        assertThat(actual.get(tp).offset()).isEqualTo(42L);
-    }
-
-    @Test
-    void listConsumerGroupOffsets_onException_returnsEmptyMap() throws Exception {
-        ListConsumerGroupOffsetsResult result = mockListConsumerGroupOffsetsResultWithException(
-                new java.util.concurrent.ExecutionException(new RuntimeException("fail")));
-        when(adminClient.listConsumerGroupOffsets("bad-group")).thenReturn(result);
-
-        Map actual = fetcher.listConsumerGroupOffsets("bad-group");
-
-        assertThat(actual).isEmpty();
-    }
-
-    @Test
-    void offsetsForTimes_returnsResults() {
-        TopicPartition tp = new TopicPartition("topic", 0);
-        Map query = Map.of(tp, 1000L);
-
-        when(consumer.offsetsForTimes(eq(query), any(Duration.class)))
-                .thenReturn(Map.of(tp, new OffsetAndTimestamp(50L, 1000L)));
-
-        Map result = fetcher.offsetsForTimes(query);
-
-        assertThat(result).hasSize(1);
-        assertThat(result.get(tp).offset()).isEqualTo(50L);
-    }
-
-    @Test
-    void offsetsForTimes_onException_returnsEmptyMap() {
-        when(consumer.offsetsForTimes(any(Map.class), any(Duration.class)))
-                .thenThrow(new RuntimeException("Error"));
-
-        Map result = fetcher.offsetsForTimes(Map.of());
-
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    void getConsumerGroupAssignmentForTopic_returnsAssignments() throws Exception {
-        TopicPartition tp0 = new TopicPartition("my-topic", 0);
-        TopicPartition tp1 = new TopicPartition("my-topic", 1);
-        TopicPartition otherTp = new TopicPartition("other-topic", 0);
-
-        MemberDescription member1 = new MemberDescription(
-                "consumer-1", Optional.empty(), "client-1", "host-1",
-                new MemberAssignment(Set.of(tp0, otherTp)));
-        MemberDescription member2 = new MemberDescription(
-                "consumer-2", Optional.empty(), "client-2", "host-2",
-                new MemberAssignment(Set.of(tp1)));
-
-        ConsumerGroupDescription groupDesc = new ConsumerGroupDescription(
-                "my-group", true, List.of(member1, member2),
-                "", ConsumerGroupState.STABLE, new Node(0, "localhost", 9092));
-
-        mockDescribeConsumerGroups("my-group", groupDesc);
-
-        Map result = fetcher.getConsumerGroupAssignmentForTopic("my-group", "my-topic");
-
-        assertThat(result).hasSize(2);
-        assertThat(result.get(0)).isEqualTo("consumer-1");
-        assertThat(result.get(1)).isEqualTo("consumer-2");
-    }
-
-    @Test
-    void getConsumerGroupAssignmentForTopic_filtersToRequestedTopic() throws Exception {
-        TopicPartition otherTp = new TopicPartition("other-topic", 0);
-
-        MemberDescription member = new MemberDescription(
-                "consumer-1", Optional.empty(), "client-1", "host-1",
-                new MemberAssignment(Set.of(otherTp)));
-
-        ConsumerGroupDescription groupDesc = new ConsumerGroupDescription(
-                "my-group", true, List.of(member),
-                "", ConsumerGroupState.STABLE, new Node(0, "localhost", 9092));
-
-        mockDescribeConsumerGroups("my-group", groupDesc);
-
-        Map result = fetcher.getConsumerGroupAssignmentForTopic("my-group", "my-topic");
-
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    void getConsumerGroupAssignmentForTopic_onException_returnsEmptyMap() throws Exception {
-        DescribeConsumerGroupsResult descResult = org.mockito.Mockito.mock(DescribeConsumerGroupsResult.class);
-        @SuppressWarnings("unchecked")
-        KafkaFuture future = org.mockito.Mockito.mock(KafkaFuture.class);
-        when(future.get(anyLong(), any(TimeUnit.class)))
-                .thenThrow(new java.util.concurrent.ExecutionException(new RuntimeException("fail")));
-        when(descResult.describedGroups()).thenReturn(Map.of("bad-group", future));
-        when(adminClient.describeConsumerGroups(List.of("bad-group"))).thenReturn(descResult);
-
-        Map result = fetcher.getConsumerGroupAssignmentForTopic("bad-group", "topic");
-
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    void close_closesConsumerAndAdmin() {
-        fetcher.close();
-
-        verify(consumer).close(any(Duration.class));
-        verify(adminClient).close(any(Duration.class));
-    }
-
-    // --- Helpers ---
-
-    private DirectOffsetFetcher createFetcherWithMocks() throws Exception {
-        sun.misc.Unsafe unsafe = getUnsafe();
-        DirectOffsetFetcher instance = (DirectOffsetFetcher) unsafe.allocateInstance(DirectOffsetFetcher.class);
-
-        setField(instance, "adminClient", adminClient);
-        setField(instance, "consumer", consumer);
-        setField(instance, "timeout", Duration.ofSeconds(30));
-
-        return instance;
-    }
-
-    private static sun.misc.Unsafe getUnsafe() throws Exception {
-        Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
-        f.setAccessible(true);
-        return (sun.misc.Unsafe) f.get(null);
-    }
-
-    private static void setField(Object target, String fieldName, Object value) throws Exception {
-        Field field = target.getClass().getDeclaredField(fieldName);
-        field.setAccessible(true);
-        field.set(target, value);
-    }
-
-    @SuppressWarnings("unchecked")
-    private ListConsumerGroupOffsetsResult mockListConsumerGroupOffsetsResult(
-            Map offsets) throws Exception {
-        ListConsumerGroupOffsetsResult result = org.mockito.Mockito.mock(ListConsumerGroupOffsetsResult.class);
-        KafkaFuture> future = org.mockito.Mockito.mock(KafkaFuture.class);
-        when(future.get(anyLong(), any(TimeUnit.class))).thenReturn(offsets);
-        when(result.partitionsToOffsetAndMetadata()).thenReturn(future);
-        return result;
-    }
-
-    @SuppressWarnings("unchecked")
-    private ListConsumerGroupOffsetsResult mockListConsumerGroupOffsetsResultWithException(
-            Exception exception) throws Exception {
-        ListConsumerGroupOffsetsResult result = org.mockito.Mockito.mock(ListConsumerGroupOffsetsResult.class);
-        KafkaFuture> future = org.mockito.Mockito.mock(KafkaFuture.class);
-        when(future.get(anyLong(), any(TimeUnit.class))).thenThrow(exception);
-        when(result.partitionsToOffsetAndMetadata()).thenReturn(future);
-        return result;
-    }
+    assertThat(fetcher.partitionCount("topic")).isEqualTo(3);
+  }
 
+  @Test
+  void partitionCount_nullPartitions_returnsZero() {
+    when(consumer.partitionsFor(any(String.class), any(Duration.class)))
+        .thenThrow(new RuntimeException("Error"));
+
+    assertThat(fetcher.partitionCount("topic")).isEqualTo(0);
+  }
+
+  @Test
+  void listConsumerGroupOffsets_returnsOffsets() throws Exception {
+    TopicPartition tp = new TopicPartition("topic", 0);
+    Map expected = Map.of(tp, new OffsetAndMetadata(42L));
+
+    ListConsumerGroupOffsetsResult result = mockListConsumerGroupOffsetsResult(expected);
+    when(adminClient.listConsumerGroupOffsets("my-group")).thenReturn(result);
+
+    Map actual = fetcher.listConsumerGroupOffsets("my-group");
+
+    assertThat(actual).hasSize(1);
+    assertThat(actual.get(tp).offset()).isEqualTo(42L);
+  }
+
+  @Test
+  void listConsumerGroupOffsets_onException_returnsEmptyMap() throws Exception {
+    ListConsumerGroupOffsetsResult result =
+        mockListConsumerGroupOffsetsResultWithException(
+            new java.util.concurrent.ExecutionException(new RuntimeException("fail")));
+    when(adminClient.listConsumerGroupOffsets("bad-group")).thenReturn(result);
+
+    Map actual = fetcher.listConsumerGroupOffsets("bad-group");
+
+    assertThat(actual).isEmpty();
+  }
+
+  @Test
+  void offsetsForTimes_returnsResults() {
+    TopicPartition tp = new TopicPartition("topic", 0);
+    Map query = Map.of(tp, 1000L);
+
+    when(consumer.offsetsForTimes(eq(query), any(Duration.class)))
+        .thenReturn(Map.of(tp, new OffsetAndTimestamp(50L, 1000L)));
+
+    Map result = fetcher.offsetsForTimes(query);
+
+    assertThat(result).hasSize(1);
+    assertThat(result.get(tp).offset()).isEqualTo(50L);
+  }
+
+  @Test
+  void offsetsForTimes_onException_returnsEmptyMap() {
+    when(consumer.offsetsForTimes(any(Map.class), any(Duration.class)))
+        .thenThrow(new RuntimeException("Error"));
+
+    Map result = fetcher.offsetsForTimes(Map.of());
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void getConsumerGroupAssignmentForTopic_returnsAssignments() throws Exception {
+    TopicPartition tp0 = new TopicPartition("my-topic", 0);
+    TopicPartition tp1 = new TopicPartition("my-topic", 1);
+    TopicPartition otherTp = new TopicPartition("other-topic", 0);
+
+    MemberDescription member1 =
+        new MemberDescription(
+            "consumer-1",
+            Optional.empty(),
+            "client-1",
+            "host-1",
+            new MemberAssignment(Set.of(tp0, otherTp)));
+    MemberDescription member2 =
+        new MemberDescription(
+            "consumer-2",
+            Optional.empty(),
+            "client-2",
+            "host-2",
+            new MemberAssignment(Set.of(tp1)));
+
+    ConsumerGroupDescription groupDesc =
+        new ConsumerGroupDescription(
+            "my-group",
+            true,
+            List.of(member1, member2),
+            "",
+            ConsumerGroupState.STABLE,
+            new Node(0, "localhost", 9092));
+
+    mockDescribeConsumerGroups("my-group", groupDesc);
+
+    Map result =
+        fetcher.getConsumerGroupAssignmentForTopic("my-group", "my-topic");
+
+    assertThat(result).hasSize(2);
+    assertThat(result.get(0)).isEqualTo("consumer-1");
+    assertThat(result.get(1)).isEqualTo("consumer-2");
+  }
+
+  @Test
+  void getConsumerGroupAssignmentForTopic_filtersToRequestedTopic() throws Exception {
+    TopicPartition otherTp = new TopicPartition("other-topic", 0);
+
+    MemberDescription member =
+        new MemberDescription(
+            "consumer-1",
+            Optional.empty(),
+            "client-1",
+            "host-1",
+            new MemberAssignment(Set.of(otherTp)));
+
+    ConsumerGroupDescription groupDesc =
+        new ConsumerGroupDescription(
+            "my-group",
+            true,
+            List.of(member),
+            "",
+            ConsumerGroupState.STABLE,
+            new Node(0, "localhost", 9092));
+
+    mockDescribeConsumerGroups("my-group", groupDesc);
+
+    Map result =
+        fetcher.getConsumerGroupAssignmentForTopic("my-group", "my-topic");
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void getConsumerGroupAssignmentForTopic_onException_returnsEmptyMap() throws Exception {
+    DescribeConsumerGroupsResult descResult =
+        org.mockito.Mockito.mock(DescribeConsumerGroupsResult.class);
     @SuppressWarnings("unchecked")
-    private void mockDescribeConsumerGroups(String groupId, ConsumerGroupDescription description) throws Exception {
-        DescribeConsumerGroupsResult descResult = org.mockito.Mockito.mock(DescribeConsumerGroupsResult.class);
-        KafkaFuture future = org.mockito.Mockito.mock(KafkaFuture.class);
-        when(future.get(anyLong(), any(TimeUnit.class))).thenReturn(description);
-        when(descResult.describedGroups()).thenReturn(Map.of(groupId, future));
-        when(adminClient.describeConsumerGroups(List.of(groupId))).thenReturn(descResult);
-    }
+    KafkaFuture future = org.mockito.Mockito.mock(KafkaFuture.class);
+    when(future.get(anyLong(), any(TimeUnit.class)))
+        .thenThrow(new java.util.concurrent.ExecutionException(new RuntimeException("fail")));
+    when(descResult.describedGroups()).thenReturn(Map.of("bad-group", future));
+    when(adminClient.describeConsumerGroups(List.of("bad-group"))).thenReturn(descResult);
+
+    Map result = fetcher.getConsumerGroupAssignmentForTopic("bad-group", "topic");
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void close_closesConsumerAndAdmin() {
+    fetcher.close();
+
+    verify(consumer).close(any(Duration.class));
+    verify(adminClient).close(any(Duration.class));
+  }
+
+  // --- Helpers ---
+
+  private DirectOffsetFetcher createFetcherWithMocks() throws Exception {
+    sun.misc.Unsafe unsafe = getUnsafe();
+    DirectOffsetFetcher instance =
+        (DirectOffsetFetcher) unsafe.allocateInstance(DirectOffsetFetcher.class);
+
+    setField(instance, "adminClient", adminClient);
+    setField(instance, "consumer", consumer);
+    setField(instance, "timeout", Duration.ofSeconds(30));
+
+    return instance;
+  }
+
+  private static sun.misc.Unsafe getUnsafe() throws Exception {
+    Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
+    f.setAccessible(true);
+    return (sun.misc.Unsafe) f.get(null);
+  }
+
+  private static void setField(Object target, String fieldName, Object value) throws Exception {
+    Field field = target.getClass().getDeclaredField(fieldName);
+    field.setAccessible(true);
+    field.set(target, value);
+  }
+
+  @SuppressWarnings("unchecked")
+  private ListConsumerGroupOffsetsResult mockListConsumerGroupOffsetsResult(
+      Map offsets) throws Exception {
+    ListConsumerGroupOffsetsResult result =
+        org.mockito.Mockito.mock(ListConsumerGroupOffsetsResult.class);
+    KafkaFuture> future =
+        org.mockito.Mockito.mock(KafkaFuture.class);
+    when(future.get(anyLong(), any(TimeUnit.class))).thenReturn(offsets);
+    when(result.partitionsToOffsetAndMetadata()).thenReturn(future);
+    return result;
+  }
+
+  @SuppressWarnings("unchecked")
+  private ListConsumerGroupOffsetsResult mockListConsumerGroupOffsetsResultWithException(
+      Exception exception) throws Exception {
+    ListConsumerGroupOffsetsResult result =
+        org.mockito.Mockito.mock(ListConsumerGroupOffsetsResult.class);
+    KafkaFuture> future =
+        org.mockito.Mockito.mock(KafkaFuture.class);
+    when(future.get(anyLong(), any(TimeUnit.class))).thenThrow(exception);
+    when(result.partitionsToOffsetAndMetadata()).thenReturn(future);
+    return result;
+  }
+
+  @SuppressWarnings("unchecked")
+  private void mockDescribeConsumerGroups(String groupId, ConsumerGroupDescription description)
+      throws Exception {
+    DescribeConsumerGroupsResult descResult =
+        org.mockito.Mockito.mock(DescribeConsumerGroupsResult.class);
+    KafkaFuture future = org.mockito.Mockito.mock(KafkaFuture.class);
+    when(future.get(anyLong(), any(TimeUnit.class))).thenReturn(description);
+    when(descResult.describedGroups()).thenReturn(Map.of(groupId, future));
+    when(adminClient.describeConsumerGroups(List.of(groupId))).thenReturn(descResult);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/metrics/MicrometerMetricsProviderTest.java b/src/test/java/com/uber/ugroup/metrics/MicrometerMetricsProviderTest.java
index 61d6558..01a7d76 100644
--- a/src/test/java/com/uber/ugroup/metrics/MicrometerMetricsProviderTest.java
+++ b/src/test/java/com/uber/ugroup/metrics/MicrometerMetricsProviderTest.java
@@ -15,198 +15,197 @@
  */
 package com.uber.ugroup.metrics;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import io.micrometer.core.instrument.Counter;
 import io.micrometer.core.instrument.Gauge;
 import io.micrometer.core.instrument.Timer;
 import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 class MicrometerMetricsProviderTest {
 
-    private SimpleMeterRegistry registry;
-    private MicrometerMetricsProvider provider;
-
-    @BeforeEach
-    void setUp() {
-        registry = new SimpleMeterRegistry();
-        provider = new MicrometerMetricsProvider(registry);
-    }
-
-    @Test
-    void incrementCounter_byOne() {
-        provider.incrementCounter("test.counter", Map.of());
-
-        Counter counter = registry.find("test.counter").counter();
-        assertThat(counter).isNotNull();
-        assertThat(counter.count()).isEqualTo(1.0);
-    }
-
-    @Test
-    void incrementCounter_byAmount() {
-        provider.incrementCounter("test.counter", 5, Map.of());
-
-        Counter counter = registry.find("test.counter").counter();
-        assertThat(counter).isNotNull();
-        assertThat(counter.count()).isEqualTo(5.0);
-    }
-
-    @Test
-    void incrementCounter_multipleTimes() {
-        provider.incrementCounter("test.counter", Map.of());
-        provider.incrementCounter("test.counter", Map.of());
-        provider.incrementCounter("test.counter", 3, Map.of());
-
-        Counter counter = registry.find("test.counter").counter();
-        assertThat(counter).isNotNull();
-        assertThat(counter.count()).isEqualTo(5.0);
-    }
-
-    @Test
-    void incrementCounter_withTags() {
-        provider.incrementCounter("test.counter", Map.of("env", "prod", "region", "us-east"));
-
-        Counter counter = registry.find("test.counter")
-                .tag("env", "prod")
-                .tag("region", "us-east")
-                .counter();
-        assertThat(counter).isNotNull();
-        assertThat(counter.count()).isEqualTo(1.0);
-    }
-
-    @Test
-    void incrementCounter_differentTags_separateCounters() {
-        provider.incrementCounter("test.counter", Map.of("env", "prod"));
-        provider.incrementCounter("test.counter", Map.of("env", "staging"));
-
-        Counter prodCounter = registry.find("test.counter").tag("env", "prod").counter();
-        Counter stagingCounter = registry.find("test.counter").tag("env", "staging").counter();
-
-        assertThat(prodCounter).isNotNull();
-        assertThat(prodCounter.count()).isEqualTo(1.0);
-        assertThat(stagingCounter).isNotNull();
-        assertThat(stagingCounter.count()).isEqualTo(1.0);
-    }
-
-    @Test
-    void registerGauge() {
-        AtomicInteger value = new AtomicInteger(42);
-        provider.registerGauge("test.gauge", value::get, Map.of());
-
-        Gauge gauge = registry.find("test.gauge").gauge();
-        assertThat(gauge).isNotNull();
-        assertThat(gauge.value()).isEqualTo(42.0);
-
-        value.set(100);
-        assertThat(gauge.value()).isEqualTo(100.0);
-    }
-
-    @Test
-    void registerGauge_withTags() {
-        provider.registerGauge("test.gauge", () -> 10, Map.of("type", "active"));
-
-        Gauge gauge = registry.find("test.gauge").tag("type", "active").gauge();
-        assertThat(gauge).isNotNull();
-        assertThat(gauge.value()).isEqualTo(10.0);
-    }
-
-    @Test
-    void registerGauge_idempotent() {
-        AtomicInteger value = new AtomicInteger(1);
-        provider.registerGauge("test.gauge", value::get, Map.of());
-        // Registering again with the same name and tags should not create a duplicate
-        provider.registerGauge("test.gauge", () -> 999, Map.of());
-
-        Gauge gauge = registry.find("test.gauge").gauge();
-        assertThat(gauge).isNotNull();
-        // Should still use the original supplier
-        assertThat(gauge.value()).isEqualTo(1.0);
-    }
-
-    @Test
-    void recordGauge() {
-        provider.recordGauge("test.record.gauge", 55.5, Map.of());
-
-        // recordGauge uses registry.gauge which tracks the value
-        assertThat(registry.find("test.record.gauge").gauge()).isNotNull();
-    }
-
-    @Test
-    void recordTimer() {
-        provider.recordTimer("test.timer", 250, Map.of());
-
-        Timer timer = registry.find("test.timer").timer();
-        assertThat(timer).isNotNull();
-        assertThat(timer.count()).isEqualTo(1);
-        assertThat(timer.totalTime(java.util.concurrent.TimeUnit.MILLISECONDS)).isEqualTo(250.0);
-    }
-
-    @Test
-    void recordTimer_multipleTimes() {
-        provider.recordTimer("test.timer", 100, Map.of());
-        provider.recordTimer("test.timer", 200, Map.of());
-
-        Timer timer = registry.find("test.timer").timer();
-        assertThat(timer).isNotNull();
-        assertThat(timer.count()).isEqualTo(2);
-        assertThat(timer.totalTime(java.util.concurrent.TimeUnit.MILLISECONDS)).isEqualTo(300.0);
-    }
-
-    @Test
-    void recordTimer_withTags() {
-        provider.recordTimer("test.timer", 500, Map.of("operation", "fetch"));
-
-        Timer timer = registry.find("test.timer").tag("operation", "fetch").timer();
-        assertThat(timer).isNotNull();
-        assertThat(timer.count()).isEqualTo(1);
-    }
-
-    @Test
-    void tagged_createsChildProvider() {
-        MetricsProvider child = provider.tagged(Map.of("cluster", "prod"));
-
-        child.incrementCounter("child.counter", Map.of());
-
-        Counter counter = registry.find("child.counter").tag("cluster", "prod").counter();
-        assertThat(counter).isNotNull();
-        assertThat(counter.count()).isEqualTo(1.0);
-    }
-
-    @Test
-    void tagged_childInheritsBaseTags() {
-        MetricsProvider child = provider.tagged(Map.of("cluster", "prod"));
-        MetricsProvider grandchild = child.tagged(Map.of("region", "us-east"));
-
-        grandchild.incrementCounter("grandchild.counter", Map.of());
-
-        Counter counter = registry.find("grandchild.counter")
-                .tag("cluster", "prod")
-                .tag("region", "us-east")
-                .counter();
-        assertThat(counter).isNotNull();
-        assertThat(counter.count()).isEqualTo(1.0);
-    }
-
-    @Test
-    void incrementCounter_emptyTags() {
-        provider.incrementCounter("no.tags.counter", Map.of());
-
-        Counter counter = registry.find("no.tags.counter").counter();
-        assertThat(counter).isNotNull();
-        assertThat(counter.count()).isEqualTo(1.0);
-    }
-
-    @Test
-    void incrementCounter_nullTags() {
-        provider.incrementCounter("null.tags.counter", null);
-
-        Counter counter = registry.find("null.tags.counter").counter();
-        assertThat(counter).isNotNull();
-        assertThat(counter.count()).isEqualTo(1.0);
-    }
+  private SimpleMeterRegistry registry;
+  private MicrometerMetricsProvider provider;
+
+  @BeforeEach
+  void setUp() {
+    registry = new SimpleMeterRegistry();
+    provider = new MicrometerMetricsProvider(registry);
+  }
+
+  @Test
+  void incrementCounter_byOne() {
+    provider.incrementCounter("test.counter", Map.of());
+
+    Counter counter = registry.find("test.counter").counter();
+    assertThat(counter).isNotNull();
+    assertThat(counter.count()).isEqualTo(1.0);
+  }
+
+  @Test
+  void incrementCounter_byAmount() {
+    provider.incrementCounter("test.counter", 5, Map.of());
+
+    Counter counter = registry.find("test.counter").counter();
+    assertThat(counter).isNotNull();
+    assertThat(counter.count()).isEqualTo(5.0);
+  }
+
+  @Test
+  void incrementCounter_multipleTimes() {
+    provider.incrementCounter("test.counter", Map.of());
+    provider.incrementCounter("test.counter", Map.of());
+    provider.incrementCounter("test.counter", 3, Map.of());
+
+    Counter counter = registry.find("test.counter").counter();
+    assertThat(counter).isNotNull();
+    assertThat(counter.count()).isEqualTo(5.0);
+  }
+
+  @Test
+  void incrementCounter_withTags() {
+    provider.incrementCounter("test.counter", Map.of("env", "prod", "region", "us-east"));
+
+    Counter counter =
+        registry.find("test.counter").tag("env", "prod").tag("region", "us-east").counter();
+    assertThat(counter).isNotNull();
+    assertThat(counter.count()).isEqualTo(1.0);
+  }
+
+  @Test
+  void incrementCounter_differentTags_separateCounters() {
+    provider.incrementCounter("test.counter", Map.of("env", "prod"));
+    provider.incrementCounter("test.counter", Map.of("env", "staging"));
+
+    Counter prodCounter = registry.find("test.counter").tag("env", "prod").counter();
+    Counter stagingCounter = registry.find("test.counter").tag("env", "staging").counter();
+
+    assertThat(prodCounter).isNotNull();
+    assertThat(prodCounter.count()).isEqualTo(1.0);
+    assertThat(stagingCounter).isNotNull();
+    assertThat(stagingCounter.count()).isEqualTo(1.0);
+  }
+
+  @Test
+  void registerGauge() {
+    AtomicInteger value = new AtomicInteger(42);
+    provider.registerGauge("test.gauge", value::get, Map.of());
+
+    Gauge gauge = registry.find("test.gauge").gauge();
+    assertThat(gauge).isNotNull();
+    assertThat(gauge.value()).isEqualTo(42.0);
+
+    value.set(100);
+    assertThat(gauge.value()).isEqualTo(100.0);
+  }
+
+  @Test
+  void registerGauge_withTags() {
+    provider.registerGauge("test.gauge", () -> 10, Map.of("type", "active"));
+
+    Gauge gauge = registry.find("test.gauge").tag("type", "active").gauge();
+    assertThat(gauge).isNotNull();
+    assertThat(gauge.value()).isEqualTo(10.0);
+  }
+
+  @Test
+  void registerGauge_idempotent() {
+    AtomicInteger value = new AtomicInteger(1);
+    provider.registerGauge("test.gauge", value::get, Map.of());
+    // Registering again with the same name and tags should not create a duplicate
+    provider.registerGauge("test.gauge", () -> 999, Map.of());
+
+    Gauge gauge = registry.find("test.gauge").gauge();
+    assertThat(gauge).isNotNull();
+    // Should still use the original supplier
+    assertThat(gauge.value()).isEqualTo(1.0);
+  }
+
+  @Test
+  void recordGauge() {
+    provider.recordGauge("test.record.gauge", 55.5, Map.of());
+
+    // recordGauge uses registry.gauge which tracks the value
+    assertThat(registry.find("test.record.gauge").gauge()).isNotNull();
+  }
+
+  @Test
+  void recordTimer() {
+    provider.recordTimer("test.timer", 250, Map.of());
+
+    Timer timer = registry.find("test.timer").timer();
+    assertThat(timer).isNotNull();
+    assertThat(timer.count()).isEqualTo(1);
+    assertThat(timer.totalTime(java.util.concurrent.TimeUnit.MILLISECONDS)).isEqualTo(250.0);
+  }
+
+  @Test
+  void recordTimer_multipleTimes() {
+    provider.recordTimer("test.timer", 100, Map.of());
+    provider.recordTimer("test.timer", 200, Map.of());
+
+    Timer timer = registry.find("test.timer").timer();
+    assertThat(timer).isNotNull();
+    assertThat(timer.count()).isEqualTo(2);
+    assertThat(timer.totalTime(java.util.concurrent.TimeUnit.MILLISECONDS)).isEqualTo(300.0);
+  }
+
+  @Test
+  void recordTimer_withTags() {
+    provider.recordTimer("test.timer", 500, Map.of("operation", "fetch"));
+
+    Timer timer = registry.find("test.timer").tag("operation", "fetch").timer();
+    assertThat(timer).isNotNull();
+    assertThat(timer.count()).isEqualTo(1);
+  }
+
+  @Test
+  void tagged_createsChildProvider() {
+    MetricsProvider child = provider.tagged(Map.of("cluster", "prod"));
+
+    child.incrementCounter("child.counter", Map.of());
+
+    Counter counter = registry.find("child.counter").tag("cluster", "prod").counter();
+    assertThat(counter).isNotNull();
+    assertThat(counter.count()).isEqualTo(1.0);
+  }
+
+  @Test
+  void tagged_childInheritsBaseTags() {
+    MetricsProvider child = provider.tagged(Map.of("cluster", "prod"));
+    MetricsProvider grandchild = child.tagged(Map.of("region", "us-east"));
+
+    grandchild.incrementCounter("grandchild.counter", Map.of());
+
+    Counter counter =
+        registry
+            .find("grandchild.counter")
+            .tag("cluster", "prod")
+            .tag("region", "us-east")
+            .counter();
+    assertThat(counter).isNotNull();
+    assertThat(counter.count()).isEqualTo(1.0);
+  }
+
+  @Test
+  void incrementCounter_emptyTags() {
+    provider.incrementCounter("no.tags.counter", Map.of());
+
+    Counter counter = registry.find("no.tags.counter").counter();
+    assertThat(counter).isNotNull();
+    assertThat(counter.count()).isEqualTo(1.0);
+  }
+
+  @Test
+  void incrementCounter_nullTags() {
+    provider.incrementCounter("null.tags.counter", null);
+
+    Counter counter = registry.find("null.tags.counter").counter();
+    assertThat(counter).isNotNull();
+    assertThat(counter.count()).isEqualTo(1.0);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/metrics/NoopMetricsProviderTest.java b/src/test/java/com/uber/ugroup/metrics/NoopMetricsProviderTest.java
index f5f34af..b38673c 100644
--- a/src/test/java/com/uber/ugroup/metrics/NoopMetricsProviderTest.java
+++ b/src/test/java/com/uber/ugroup/metrics/NoopMetricsProviderTest.java
@@ -15,65 +15,64 @@
  */
 package com.uber.ugroup.metrics;
 
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
 
 class NoopMetricsProviderTest {
 
-    @Test
-    void singletonInstance() {
-        assertThat(NoopMetricsProvider.INSTANCE).isNotNull();
-        assertThat(NoopMetricsProvider.INSTANCE).isSameAs(NoopMetricsProvider.INSTANCE);
-    }
+  @Test
+  void singletonInstance() {
+    assertThat(NoopMetricsProvider.INSTANCE).isNotNull();
+    assertThat(NoopMetricsProvider.INSTANCE).isSameAs(NoopMetricsProvider.INSTANCE);
+  }
 
-    @Test
-    void singletonInstance_isMetricsProvider() {
-        assertThat(NoopMetricsProvider.INSTANCE).isInstanceOf(MetricsProvider.class);
-    }
+  @Test
+  void singletonInstance_isMetricsProvider() {
+    assertThat(NoopMetricsProvider.INSTANCE).isInstanceOf(MetricsProvider.class);
+  }
 
-    @Test
-    void incrementCounter_doesNotThrow() {
-        NoopMetricsProvider.INSTANCE.incrementCounter("test.counter", Map.of("key", "value"));
-    }
+  @Test
+  void incrementCounter_doesNotThrow() {
+    NoopMetricsProvider.INSTANCE.incrementCounter("test.counter", Map.of("key", "value"));
+  }
 
-    @Test
-    void incrementCounter_withCount_doesNotThrow() {
-        NoopMetricsProvider.INSTANCE.incrementCounter("test.counter", 5, Map.of("key", "value"));
-    }
+  @Test
+  void incrementCounter_withCount_doesNotThrow() {
+    NoopMetricsProvider.INSTANCE.incrementCounter("test.counter", 5, Map.of("key", "value"));
+  }
 
-    @Test
-    void incrementCounter_emptyTags_doesNotThrow() {
-        NoopMetricsProvider.INSTANCE.incrementCounter("test.counter", Map.of());
-        NoopMetricsProvider.INSTANCE.incrementCounter("test.counter", 10, Map.of());
-    }
+  @Test
+  void incrementCounter_emptyTags_doesNotThrow() {
+    NoopMetricsProvider.INSTANCE.incrementCounter("test.counter", Map.of());
+    NoopMetricsProvider.INSTANCE.incrementCounter("test.counter", 10, Map.of());
+  }
 
-    @Test
-    void registerGauge_doesNotThrow() {
-        NoopMetricsProvider.INSTANCE.registerGauge("test.gauge", () -> 42, Map.of("key", "value"));
-    }
+  @Test
+  void registerGauge_doesNotThrow() {
+    NoopMetricsProvider.INSTANCE.registerGauge("test.gauge", () -> 42, Map.of("key", "value"));
+  }
 
-    @Test
-    void recordGauge_doesNotThrow() {
-        NoopMetricsProvider.INSTANCE.recordGauge("test.gauge", 3.14, Map.of("key", "value"));
-    }
+  @Test
+  void recordGauge_doesNotThrow() {
+    NoopMetricsProvider.INSTANCE.recordGauge("test.gauge", 3.14, Map.of("key", "value"));
+  }
 
-    @Test
-    void recordTimer_doesNotThrow() {
-        NoopMetricsProvider.INSTANCE.recordTimer("test.timer", 100L, Map.of("key", "value"));
-    }
+  @Test
+  void recordTimer_doesNotThrow() {
+    NoopMetricsProvider.INSTANCE.recordTimer("test.timer", 100L, Map.of("key", "value"));
+  }
 
-    @Test
-    void tagged_returnsSelf() {
-        MetricsProvider tagged = NoopMetricsProvider.INSTANCE.tagged(Map.of("env", "test"));
-        assertThat(tagged).isSameAs(NoopMetricsProvider.INSTANCE);
-    }
+  @Test
+  void tagged_returnsSelf() {
+    MetricsProvider tagged = NoopMetricsProvider.INSTANCE.tagged(Map.of("env", "test"));
+    assertThat(tagged).isSameAs(NoopMetricsProvider.INSTANCE);
+  }
 
-    @Test
-    void tagged_emptyTags_returnsSelf() {
-        MetricsProvider tagged = NoopMetricsProvider.INSTANCE.tagged(Map.of());
-        assertThat(tagged).isSameAs(NoopMetricsProvider.INSTANCE);
-    }
+  @Test
+  void tagged_emptyTags_returnsSelf() {
+    MetricsProvider tagged = NoopMetricsProvider.INSTANCE.tagged(Map.of());
+    assertThat(tagged).isSameAs(NoopMetricsProvider.INSTANCE);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/model/ConsumerLagStateTest.java b/src/test/java/com/uber/ugroup/model/ConsumerLagStateTest.java
index 51bd53b..a7c16ef 100644
--- a/src/test/java/com/uber/ugroup/model/ConsumerLagStateTest.java
+++ b/src/test/java/com/uber/ugroup/model/ConsumerLagStateTest.java
@@ -15,320 +15,296 @@
  */
 package com.uber.ugroup.model;
 
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import java.time.Duration;
 import java.util.Map;
-import java.util.Optional;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
 
 class ConsumerLagStateTest {
 
-    @Test
-    void constructor_setsGroupAndTopic() {
-        GroupAndTopic gat = new GroupAndTopic("my-group", "my-topic");
-        ConsumerLagState state = new ConsumerLagState(gat, Map.of());
-
-        assertThat(state.getGroupAndTopic()).isEqualTo(gat);
-    }
-
-    @Test
-    void constructor_setsComputedAtMillis() {
-        long before = System.currentTimeMillis();
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of());
-        long after = System.currentTimeMillis();
-
-        assertThat(state.getComputedAtMillis()).isBetween(before, after);
-    }
-
-    @Test
-    void getPartitionLagStates_returnsUnmodifiableMap() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .offsetLag(10)
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.getPartitionLagStates()).hasSize(1);
-        assertThat(state.getPartitionLagStates().get(0).getOffsetLag()).isEqualTo(10);
-    }
-
-    @Test
-    void getPartitionLagStates_isDefensiveCopy() {
-        Map mutable = new java.util.HashMap<>();
-        mutable.put(0, ConsumerLagState.PartitionLagState.builder(0).offsetLag(5).build());
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), mutable);
-
-        // Mutating original should not affect state
-        mutable.put(1, ConsumerLagState.PartitionLagState.builder(1).offsetLag(99).build());
-        assertThat(state.getPartitionLagStates()).hasSize(1);
-    }
-
-    @Test
-    void getTotalOffsetLag_emptyPartitions() {
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of());
-
-        assertThat(state.getTotalOffsetLag()).isEqualTo(0);
-    }
-
-    @Test
-    void getTotalOffsetLag_singlePartition() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .offsetLag(100)
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.getTotalOffsetLag()).isEqualTo(100);
-    }
-
-    @Test
-    void getTotalOffsetLag_multiplePartitions() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .offsetLag(100)
-                .build();
-        ConsumerLagState.PartitionLagState p1 = ConsumerLagState.PartitionLagState.builder(1)
-                .offsetLag(200)
-                .build();
-        ConsumerLagState.PartitionLagState p2 = ConsumerLagState.PartitionLagState.builder(2)
-                .offsetLag(300)
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1, 2, p2));
-
-        assertThat(state.getTotalOffsetLag()).isEqualTo(600);
-    }
-
-    @Test
-    void getMaxTimeLag_emptyPartitions() {
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of());
-
-        assertThat(state.getMaxTimeLag()).isEmpty();
-    }
-
-    @Test
-    void getMaxTimeLag_noTimeLagSet() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .offsetLag(100)
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.getMaxTimeLag()).isEmpty();
-    }
-
-    @Test
-    void getMaxTimeLag_singlePartition() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .timeLag(Duration.ofSeconds(30))
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.getMaxTimeLag()).isPresent();
-        assertThat(state.getMaxTimeLag().get()).isEqualTo(Duration.ofSeconds(30));
-    }
-
-    @Test
-    void getMaxTimeLag_multiplePartitions_returnsMax() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .timeLag(Duration.ofSeconds(10))
-                .build();
-        ConsumerLagState.PartitionLagState p1 = ConsumerLagState.PartitionLagState.builder(1)
-                .timeLag(Duration.ofSeconds(60))
-                .build();
-        ConsumerLagState.PartitionLagState p2 = ConsumerLagState.PartitionLagState.builder(2)
-                .timeLag(Duration.ofSeconds(30))
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1, 2, p2));
-
-        assertThat(state.getMaxTimeLag()).isPresent();
-        assertThat(state.getMaxTimeLag().get()).isEqualTo(Duration.ofSeconds(60));
-    }
-
-    @Test
-    void getMaxTimeLag_mixedPresence() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .timeLag(Duration.ofSeconds(10))
-                .build();
-        ConsumerLagState.PartitionLagState p1 = ConsumerLagState.PartitionLagState.builder(1)
-                .build(); // no time lag
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1));
-
-        assertThat(state.getMaxTimeLag()).isPresent();
-        assertThat(state.getMaxTimeLag().get()).isEqualTo(Duration.ofSeconds(10));
-    }
-
-    @Test
-    void getAveragePercentageLag_emptyPartitions() {
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of());
-
-        assertThat(state.getAveragePercentageLag()).isEqualTo(0.0);
-    }
-
-    @Test
-    void getAveragePercentageLag_singlePartition() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .percentageLag(50.0)
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.getAveragePercentageLag()).isEqualTo(50.0);
-    }
-
-    @Test
-    void getAveragePercentageLag_multiplePartitions() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .percentageLag(10.0)
-                .build();
-        ConsumerLagState.PartitionLagState p1 = ConsumerLagState.PartitionLagState.builder(1)
-                .percentageLag(30.0)
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1));
-
-        assertThat(state.getAveragePercentageLag()).isEqualTo(20.0);
-    }
-
-    @Test
-    void isFreshnessSLABreached_emptyPartitions() {
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of());
-
-        assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isFalse();
-    }
-
-    @Test
-    void isFreshnessSLABreached_noTimeSinceLastCommit() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .offsetLag(100)
-                .build(); // no timeSinceLastCommit
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isFalse();
-    }
-
-    @Test
-    void isFreshnessSLABreached_withinSLA() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .timeSinceLastCommit(Duration.ofMinutes(3))
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isFalse();
-    }
-
-    @Test
-    void isFreshnessSLABreached_exceedsSLA() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .timeSinceLastCommit(Duration.ofMinutes(10))
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isTrue();
-    }
-
-    @Test
-    void isFreshnessSLABreached_exactlySLA() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .timeSinceLastCommit(Duration.ofMinutes(5))
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0));
-
-        assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isFalse();
-    }
-
-    @Test
-    void isFreshnessSLABreached_anyPartitionBreaches() {
-        ConsumerLagState.PartitionLagState p0 = ConsumerLagState.PartitionLagState.builder(0)
-                .timeSinceLastCommit(Duration.ofMinutes(1))
-                .build();
-        ConsumerLagState.PartitionLagState p1 = ConsumerLagState.PartitionLagState.builder(1)
-                .timeSinceLastCommit(Duration.ofMinutes(10))
-                .build();
-
-        ConsumerLagState state = new ConsumerLagState(
-                new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1));
-
-        assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isTrue();
-    }
-
-    // --- PartitionLagState builder tests ---
-
-    @Test
-    void partitionLagState_builder_setsAllFields() {
-        ConsumerLagState.PartitionLagState pls = ConsumerLagState.PartitionLagState.builder(3)
-                .committedOffset(500)
-                .endOffset(1000)
-                .beginningOffset(100)
-                .offsetLag(500)
-                .percentageLag(55.5)
-                .timeLag(Duration.ofSeconds(45))
-                .timeSinceLastCommit(Duration.ofSeconds(10))
-                .build();
-
-        assertThat(pls.getPartition()).isEqualTo(3);
-        assertThat(pls.getCommittedOffset()).isEqualTo(500);
-        assertThat(pls.getEndOffset()).isEqualTo(1000);
-        assertThat(pls.getBeginningOffset()).isEqualTo(100);
-        assertThat(pls.getOffsetLag()).isEqualTo(500);
-        assertThat(pls.getPercentageLag()).isEqualTo(55.5);
-        assertThat(pls.getTimeLag()).isPresent().contains(Duration.ofSeconds(45));
-        assertThat(pls.getTimeSinceLastCommit()).isPresent().contains(Duration.ofSeconds(10));
-    }
-
-    @Test
-    void partitionLagState_builder_defaults() {
-        ConsumerLagState.PartitionLagState pls = ConsumerLagState.PartitionLagState.builder(0).build();
-
-        assertThat(pls.getPartition()).isEqualTo(0);
-        assertThat(pls.getCommittedOffset()).isEqualTo(0);
-        assertThat(pls.getEndOffset()).isEqualTo(0);
-        assertThat(pls.getBeginningOffset()).isEqualTo(0);
-        assertThat(pls.getOffsetLag()).isEqualTo(0);
-        assertThat(pls.getPercentageLag()).isEqualTo(0.0);
-        assertThat(pls.getTimeLag()).isEmpty();
-        assertThat(pls.getTimeSinceLastCommit()).isEmpty();
-    }
-
-    @Test
-    void partitionLagState_getTimeLag_empty_whenNull() {
-        ConsumerLagState.PartitionLagState pls = ConsumerLagState.PartitionLagState.builder(0).build();
-
-        assertThat(pls.getTimeLag()).isEmpty();
-    }
-
-    @Test
-    void partitionLagState_getTimeSinceLastCommit_empty_whenNull() {
-        ConsumerLagState.PartitionLagState pls = ConsumerLagState.PartitionLagState.builder(0).build();
-
-        assertThat(pls.getTimeSinceLastCommit()).isEmpty();
-    }
+  @Test
+  void constructor_setsGroupAndTopic() {
+    GroupAndTopic gat = new GroupAndTopic("my-group", "my-topic");
+    ConsumerLagState state = new ConsumerLagState(gat, Map.of());
+
+    assertThat(state.getGroupAndTopic()).isEqualTo(gat);
+  }
+
+  @Test
+  void constructor_setsComputedAtMillis() {
+    long before = System.currentTimeMillis();
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of());
+    long after = System.currentTimeMillis();
+
+    assertThat(state.getComputedAtMillis()).isBetween(before, after);
+  }
+
+  @Test
+  void getPartitionLagStates_returnsUnmodifiableMap() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).offsetLag(10).build();
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.getPartitionLagStates()).hasSize(1);
+    assertThat(state.getPartitionLagStates().get(0).getOffsetLag()).isEqualTo(10);
+  }
+
+  @Test
+  void getPartitionLagStates_isDefensiveCopy() {
+    Map mutable = new java.util.HashMap<>();
+    mutable.put(0, ConsumerLagState.PartitionLagState.builder(0).offsetLag(5).build());
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), mutable);
+
+    // Mutating original should not affect state
+    mutable.put(1, ConsumerLagState.PartitionLagState.builder(1).offsetLag(99).build());
+    assertThat(state.getPartitionLagStates()).hasSize(1);
+  }
+
+  @Test
+  void getTotalOffsetLag_emptyPartitions() {
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of());
+
+    assertThat(state.getTotalOffsetLag()).isEqualTo(0);
+  }
+
+  @Test
+  void getTotalOffsetLag_singlePartition() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).offsetLag(100).build();
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.getTotalOffsetLag()).isEqualTo(100);
+  }
+
+  @Test
+  void getTotalOffsetLag_multiplePartitions() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).offsetLag(100).build();
+    ConsumerLagState.PartitionLagState p1 =
+        ConsumerLagState.PartitionLagState.builder(1).offsetLag(200).build();
+    ConsumerLagState.PartitionLagState p2 =
+        ConsumerLagState.PartitionLagState.builder(2).offsetLag(300).build();
+
+    ConsumerLagState state =
+        new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1, 2, p2));
+
+    assertThat(state.getTotalOffsetLag()).isEqualTo(600);
+  }
+
+  @Test
+  void getMaxTimeLag_emptyPartitions() {
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of());
+
+    assertThat(state.getMaxTimeLag()).isEmpty();
+  }
+
+  @Test
+  void getMaxTimeLag_noTimeLagSet() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).offsetLag(100).build();
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.getMaxTimeLag()).isEmpty();
+  }
+
+  @Test
+  void getMaxTimeLag_singlePartition() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).timeLag(Duration.ofSeconds(30)).build();
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.getMaxTimeLag()).isPresent();
+    assertThat(state.getMaxTimeLag().get()).isEqualTo(Duration.ofSeconds(30));
+  }
+
+  @Test
+  void getMaxTimeLag_multiplePartitions_returnsMax() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).timeLag(Duration.ofSeconds(10)).build();
+    ConsumerLagState.PartitionLagState p1 =
+        ConsumerLagState.PartitionLagState.builder(1).timeLag(Duration.ofSeconds(60)).build();
+    ConsumerLagState.PartitionLagState p2 =
+        ConsumerLagState.PartitionLagState.builder(2).timeLag(Duration.ofSeconds(30)).build();
+
+    ConsumerLagState state =
+        new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1, 2, p2));
+
+    assertThat(state.getMaxTimeLag()).isPresent();
+    assertThat(state.getMaxTimeLag().get()).isEqualTo(Duration.ofSeconds(60));
+  }
+
+  @Test
+  void getMaxTimeLag_mixedPresence() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).timeLag(Duration.ofSeconds(10)).build();
+    ConsumerLagState.PartitionLagState p1 =
+        ConsumerLagState.PartitionLagState.builder(1).build(); // no time lag
+
+    ConsumerLagState state =
+        new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1));
+
+    assertThat(state.getMaxTimeLag()).isPresent();
+    assertThat(state.getMaxTimeLag().get()).isEqualTo(Duration.ofSeconds(10));
+  }
+
+  @Test
+  void getAveragePercentageLag_emptyPartitions() {
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of());
+
+    assertThat(state.getAveragePercentageLag()).isEqualTo(0.0);
+  }
+
+  @Test
+  void getAveragePercentageLag_singlePartition() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).percentageLag(50.0).build();
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.getAveragePercentageLag()).isEqualTo(50.0);
+  }
+
+  @Test
+  void getAveragePercentageLag_multiplePartitions() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0).percentageLag(10.0).build();
+    ConsumerLagState.PartitionLagState p1 =
+        ConsumerLagState.PartitionLagState.builder(1).percentageLag(30.0).build();
+
+    ConsumerLagState state =
+        new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1));
+
+    assertThat(state.getAveragePercentageLag()).isEqualTo(20.0);
+  }
+
+  @Test
+  void isFreshnessSLABreached_emptyPartitions() {
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of());
+
+    assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isFalse();
+  }
+
+  @Test
+  void isFreshnessSLABreached_noTimeSinceLastCommit() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0)
+            .offsetLag(100)
+            .build(); // no timeSinceLastCommit
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isFalse();
+  }
+
+  @Test
+  void isFreshnessSLABreached_withinSLA() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0)
+            .timeSinceLastCommit(Duration.ofMinutes(3))
+            .build();
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isFalse();
+  }
+
+  @Test
+  void isFreshnessSLABreached_exceedsSLA() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0)
+            .timeSinceLastCommit(Duration.ofMinutes(10))
+            .build();
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isTrue();
+  }
+
+  @Test
+  void isFreshnessSLABreached_exactlySLA() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0)
+            .timeSinceLastCommit(Duration.ofMinutes(5))
+            .build();
+
+    ConsumerLagState state = new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0));
+
+    assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isFalse();
+  }
+
+  @Test
+  void isFreshnessSLABreached_anyPartitionBreaches() {
+    ConsumerLagState.PartitionLagState p0 =
+        ConsumerLagState.PartitionLagState.builder(0)
+            .timeSinceLastCommit(Duration.ofMinutes(1))
+            .build();
+    ConsumerLagState.PartitionLagState p1 =
+        ConsumerLagState.PartitionLagState.builder(1)
+            .timeSinceLastCommit(Duration.ofMinutes(10))
+            .build();
+
+    ConsumerLagState state =
+        new ConsumerLagState(new GroupAndTopic("g", "t"), Map.of(0, p0, 1, p1));
+
+    assertThat(state.isFreshnessSLABreached(Duration.ofMinutes(5))).isTrue();
+  }
+
+  // --- PartitionLagState builder tests ---
+
+  @Test
+  void partitionLagState_builder_setsAllFields() {
+    ConsumerLagState.PartitionLagState pls =
+        ConsumerLagState.PartitionLagState.builder(3)
+            .committedOffset(500)
+            .endOffset(1000)
+            .beginningOffset(100)
+            .offsetLag(500)
+            .percentageLag(55.5)
+            .timeLag(Duration.ofSeconds(45))
+            .timeSinceLastCommit(Duration.ofSeconds(10))
+            .build();
+
+    assertThat(pls.getPartition()).isEqualTo(3);
+    assertThat(pls.getCommittedOffset()).isEqualTo(500);
+    assertThat(pls.getEndOffset()).isEqualTo(1000);
+    assertThat(pls.getBeginningOffset()).isEqualTo(100);
+    assertThat(pls.getOffsetLag()).isEqualTo(500);
+    assertThat(pls.getPercentageLag()).isEqualTo(55.5);
+    assertThat(pls.getTimeLag()).isPresent().contains(Duration.ofSeconds(45));
+    assertThat(pls.getTimeSinceLastCommit()).isPresent().contains(Duration.ofSeconds(10));
+  }
+
+  @Test
+  void partitionLagState_builder_defaults() {
+    ConsumerLagState.PartitionLagState pls = ConsumerLagState.PartitionLagState.builder(0).build();
+
+    assertThat(pls.getPartition()).isEqualTo(0);
+    assertThat(pls.getCommittedOffset()).isEqualTo(0);
+    assertThat(pls.getEndOffset()).isEqualTo(0);
+    assertThat(pls.getBeginningOffset()).isEqualTo(0);
+    assertThat(pls.getOffsetLag()).isEqualTo(0);
+    assertThat(pls.getPercentageLag()).isEqualTo(0.0);
+    assertThat(pls.getTimeLag()).isEmpty();
+    assertThat(pls.getTimeSinceLastCommit()).isEmpty();
+  }
+
+  @Test
+  void partitionLagState_getTimeLag_empty_whenNull() {
+    ConsumerLagState.PartitionLagState pls = ConsumerLagState.PartitionLagState.builder(0).build();
+
+    assertThat(pls.getTimeLag()).isEmpty();
+  }
+
+  @Test
+  void partitionLagState_getTimeSinceLastCommit_empty_whenNull() {
+    ConsumerLagState.PartitionLagState pls = ConsumerLagState.PartitionLagState.builder(0).build();
+
+    assertThat(pls.getTimeSinceLastCommit()).isEmpty();
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/model/ConsumerMetadataTest.java b/src/test/java/com/uber/ugroup/model/ConsumerMetadataTest.java
index d575ee6..7012f52 100644
--- a/src/test/java/com/uber/ugroup/model/ConsumerMetadataTest.java
+++ b/src/test/java/com/uber/ugroup/model/ConsumerMetadataTest.java
@@ -15,61 +15,58 @@
  */
 package com.uber.ugroup.model;
 
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
 
 class ConsumerMetadataTest {
 
-    @Test
-    void builder_defaults() {
-        ConsumerMetadata metadata = ConsumerMetadata.builder().build();
+  @Test
+  void builder_defaults() {
+    ConsumerMetadata metadata = ConsumerMetadata.builder().build();
 
-        assertThat(metadata.getConsumerType()).isEqualTo("default");
-        assertThat(metadata.isEnabled()).isTrue();
-        assertThat(metadata.getCluster()).isNull();
-        assertThat(metadata.getAdditionalTags()).isEmpty();
-    }
+    assertThat(metadata.getConsumerType()).isEqualTo("default");
+    assertThat(metadata.isEnabled()).isTrue();
+    assertThat(metadata.getCluster()).isNull();
+    assertThat(metadata.getAdditionalTags()).isEmpty();
+  }
 
-    @Test
-    void builder_allFields() {
-        ConsumerMetadata metadata = ConsumerMetadata.builder()
-                .consumerType("batch")
-                .cluster("prod-cluster")
-                .enabled(false)
-                .additionalTags(Map.of("team", "data"))
-                .build();
+  @Test
+  void builder_allFields() {
+    ConsumerMetadata metadata =
+        ConsumerMetadata.builder()
+            .consumerType("batch")
+            .cluster("prod-cluster")
+            .enabled(false)
+            .additionalTags(Map.of("team", "data"))
+            .build();
 
-        assertThat(metadata.getConsumerType()).isEqualTo("batch");
-        assertThat(metadata.getCluster()).isEqualTo("prod-cluster");
-        assertThat(metadata.isEnabled()).isFalse();
-        assertThat(metadata.getAdditionalTags()).containsEntry("team", "data");
-    }
+    assertThat(metadata.getConsumerType()).isEqualTo("batch");
+    assertThat(metadata.getCluster()).isEqualTo("prod-cluster");
+    assertThat(metadata.isEnabled()).isFalse();
+    assertThat(metadata.getAdditionalTags()).containsEntry("team", "data");
+  }
 
-    @Test
-    void getTag_present() {
-        ConsumerMetadata metadata = ConsumerMetadata.builder()
-                .additionalTags(Map.of("key", "value"))
-                .build();
+  @Test
+  void getTag_present() {
+    ConsumerMetadata metadata =
+        ConsumerMetadata.builder().additionalTags(Map.of("key", "value")).build();
 
-        assertThat(metadata.getTag("key")).isPresent().contains("value");
-    }
+    assertThat(metadata.getTag("key")).isPresent().contains("value");
+  }
 
-    @Test
-    void getTag_absent() {
-        ConsumerMetadata metadata = ConsumerMetadata.builder().build();
+  @Test
+  void getTag_absent() {
+    ConsumerMetadata metadata = ConsumerMetadata.builder().build();
 
-        assertThat(metadata.getTag("missing")).isEmpty();
-    }
+    assertThat(metadata.getTag("missing")).isEmpty();
+  }
 
-    @Test
-    void toString_containsType() {
-        ConsumerMetadata metadata = ConsumerMetadata.builder()
-                .consumerType("streaming")
-                .build();
+  @Test
+  void toString_containsType() {
+    ConsumerMetadata metadata = ConsumerMetadata.builder().consumerType("streaming").build();
 
-        assertThat(metadata.toString()).contains("streaming");
-    }
+    assertThat(metadata.toString()).contains("streaming");
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/model/GroupAndTopicTest.java b/src/test/java/com/uber/ugroup/model/GroupAndTopicTest.java
index 30697e4..8840ce1 100644
--- a/src/test/java/com/uber/ugroup/model/GroupAndTopicTest.java
+++ b/src/test/java/com/uber/ugroup/model/GroupAndTopicTest.java
@@ -15,73 +15,70 @@
  */
 package com.uber.ugroup.model;
 
-import org.junit.jupiter.api.Test;
-
 import static org.assertj.core.api.Assertions.assertThat;
 
+import org.junit.jupiter.api.Test;
+
 class GroupAndTopicTest {
 
-    @Test
-    void partitionCalculation_consistent() {
-        GroupAndTopic gat1 = new GroupAndTopic("my-group", "my-topic");
-        GroupAndTopic gat2 = new GroupAndTopic("my-group", "my-topic");
-
-        assertThat(gat1.getConsumerOffsetsPartition())
-                .isEqualTo(gat2.getConsumerOffsetsPartition());
-    }
-
-    @Test
-    void partitionCalculation_differentGroups() {
-        GroupAndTopic gat1 = new GroupAndTopic("group-a", "topic");
-        GroupAndTopic gat2 = new GroupAndTopic("group-b", "topic");
-
-        // Different groups should (usually) map to different partitions
-        // Note: This isn't guaranteed due to hash collisions, but is expected
-        assertThat(gat1.getConsumerOffsetsPartition())
-                .isLessThan(50);
-        assertThat(gat2.getConsumerOffsetsPartition())
-                .isLessThan(50);
-    }
-
-    @Test
-    void partitionCalculation_customPartitionCount() {
-        int partition = GroupAndTopic.calculatePartition("test-group", 100);
-        assertThat(partition).isBetween(0, 99);
-    }
-
-    @Test
-    void equals_sameGroupTopic() {
-        GroupAndTopic gat1 = new GroupAndTopic("group", "topic");
-        GroupAndTopic gat2 = new GroupAndTopic("group", "topic");
-
-        assertThat(gat1).isEqualTo(gat2);
-        assertThat(gat1.hashCode()).isEqualTo(gat2.hashCode());
-    }
-
-    @Test
-    void equals_differentGroup() {
-        GroupAndTopic gat1 = new GroupAndTopic("group-a", "topic");
-        GroupAndTopic gat2 = new GroupAndTopic("group-b", "topic");
-
-        assertThat(gat1).isNotEqualTo(gat2);
-    }
-
-    @Test
-    void equals_differentTopic() {
-        GroupAndTopic gat1 = new GroupAndTopic("group", "topic-a");
-        GroupAndTopic gat2 = new GroupAndTopic("group", "topic-b");
-
-        assertThat(gat1).isNotEqualTo(gat2);
-    }
-
-    @Test
-    void withPartitionCount() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        GroupAndTopic updated = gat.withPartitionCount(100);
-
-        assertThat(updated.getGroup()).isEqualTo("group");
-        assertThat(updated.getTopic()).isEqualTo("topic");
-        assertThat(updated.getConsumerOffsetsPartition())
-                .isEqualTo(GroupAndTopic.calculatePartition("group", 100));
-    }
+  @Test
+  void partitionCalculation_consistent() {
+    GroupAndTopic gat1 = new GroupAndTopic("my-group", "my-topic");
+    GroupAndTopic gat2 = new GroupAndTopic("my-group", "my-topic");
+
+    assertThat(gat1.getConsumerOffsetsPartition()).isEqualTo(gat2.getConsumerOffsetsPartition());
+  }
+
+  @Test
+  void partitionCalculation_differentGroups() {
+    GroupAndTopic gat1 = new GroupAndTopic("group-a", "topic");
+    GroupAndTopic gat2 = new GroupAndTopic("group-b", "topic");
+
+    // Different groups should (usually) map to different partitions
+    // Note: This isn't guaranteed due to hash collisions, but is expected
+    assertThat(gat1.getConsumerOffsetsPartition()).isLessThan(50);
+    assertThat(gat2.getConsumerOffsetsPartition()).isLessThan(50);
+  }
+
+  @Test
+  void partitionCalculation_customPartitionCount() {
+    int partition = GroupAndTopic.calculatePartition("test-group", 100);
+    assertThat(partition).isBetween(0, 99);
+  }
+
+  @Test
+  void equals_sameGroupTopic() {
+    GroupAndTopic gat1 = new GroupAndTopic("group", "topic");
+    GroupAndTopic gat2 = new GroupAndTopic("group", "topic");
+
+    assertThat(gat1).isEqualTo(gat2);
+    assertThat(gat1.hashCode()).isEqualTo(gat2.hashCode());
+  }
+
+  @Test
+  void equals_differentGroup() {
+    GroupAndTopic gat1 = new GroupAndTopic("group-a", "topic");
+    GroupAndTopic gat2 = new GroupAndTopic("group-b", "topic");
+
+    assertThat(gat1).isNotEqualTo(gat2);
+  }
+
+  @Test
+  void equals_differentTopic() {
+    GroupAndTopic gat1 = new GroupAndTopic("group", "topic-a");
+    GroupAndTopic gat2 = new GroupAndTopic("group", "topic-b");
+
+    assertThat(gat1).isNotEqualTo(gat2);
+  }
+
+  @Test
+  void withPartitionCount() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    GroupAndTopic updated = gat.withPartitionCount(100);
+
+    assertThat(updated.getGroup()).isEqualTo("group");
+    assertThat(updated.getTopic()).isEqualTo("topic");
+    assertThat(updated.getConsumerOffsetsPartition())
+        .isEqualTo(GroupAndTopic.calculatePartition("group", 100));
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/model/LastCommittedOffsetTest.java b/src/test/java/com/uber/ugroup/model/LastCommittedOffsetTest.java
index 926f3ec..3d6c86b 100644
--- a/src/test/java/com/uber/ugroup/model/LastCommittedOffsetTest.java
+++ b/src/test/java/com/uber/ugroup/model/LastCommittedOffsetTest.java
@@ -15,89 +15,90 @@
  */
 package com.uber.ugroup.model;
 
-import org.junit.jupiter.api.Test;
-
 import static org.assertj.core.api.Assertions.assertThat;
 
+import org.junit.jupiter.api.Test;
+
 class LastCommittedOffsetTest {
 
-    @Test
-    void constructor_setsFields() {
-        LastCommittedOffset offset = new LastCommittedOffset(100, 123456789L);
+  @Test
+  void constructor_setsFields() {
+    LastCommittedOffset offset = new LastCommittedOffset(100, 123456789L);
 
-        assertThat(offset.getOffset()).isEqualTo(100);
-        assertThat(offset.getCommitTimeMillis()).isEqualTo(123456789L);
-    }
+    assertThat(offset.getOffset()).isEqualTo(100);
+    assertThat(offset.getCommitTimeMillis()).isEqualTo(123456789L);
+  }
 
-    @Test
-    void constructor_zeroValues() {
-        LastCommittedOffset offset = new LastCommittedOffset(0, 0);
+  @Test
+  void constructor_zeroValues() {
+    LastCommittedOffset offset = new LastCommittedOffset(0, 0);
 
-        assertThat(offset.getOffset()).isEqualTo(0);
-        assertThat(offset.getCommitTimeMillis()).isEqualTo(0);
-    }
+    assertThat(offset.getOffset()).isEqualTo(0);
+    assertThat(offset.getCommitTimeMillis()).isEqualTo(0);
+  }
 
-    @Test
-    void now_setsCurrentTime() {
-        long before = System.currentTimeMillis();
-        LastCommittedOffset offset = LastCommittedOffset.now(42);
-        long after = System.currentTimeMillis();
+  @Test
+  void now_setsCurrentTime() {
+    long before = System.currentTimeMillis();
+    LastCommittedOffset offset = LastCommittedOffset.now(42);
+    long after = System.currentTimeMillis();
 
-        assertThat(offset.getOffset()).isEqualTo(42);
-        assertThat(offset.getCommitTimeMillis()).isBetween(before, after);
-    }
+    assertThat(offset.getOffset()).isEqualTo(42);
+    assertThat(offset.getCommitTimeMillis()).isBetween(before, after);
+  }
 
-    @Test
-    void getTimeSinceCommitMillis_returnsPositive_forPastCommit() {
-        long pastTime = System.currentTimeMillis() - 5000;
-        LastCommittedOffset offset = new LastCommittedOffset(100, pastTime);
+  @Test
+  void getTimeSinceCommitMillis_returnsPositive_forPastCommit() {
+    long pastTime = System.currentTimeMillis() - 5000;
+    LastCommittedOffset offset = new LastCommittedOffset(100, pastTime);
 
-        long timeSince = offset.getTimeSinceCommitMillis();
+    long timeSince = offset.getTimeSinceCommitMillis();
 
-        assertThat(timeSince).isGreaterThanOrEqualTo(5000);
-    }
+    assertThat(timeSince).isGreaterThanOrEqualTo(5000);
+  }
 
-    @Test
-    void getTimeSinceCommitMillis_returnsSmallValue_forRecentCommit() {
-        LastCommittedOffset offset = LastCommittedOffset.now(100);
+  @Test
+  void getTimeSinceCommitMillis_returnsSmallValue_forRecentCommit() {
+    LastCommittedOffset offset = LastCommittedOffset.now(100);
 
-        long timeSince = offset.getTimeSinceCommitMillis();
+    long timeSince = offset.getTimeSinceCommitMillis();
 
-        assertThat(timeSince).isGreaterThanOrEqualTo(0);
-        assertThat(timeSince).isLessThan(1000);
-    }
+    assertThat(timeSince).isGreaterThanOrEqualTo(0);
+    assertThat(timeSince).isLessThan(1000);
+  }
 
-    @Test
-    void toString_containsOffsetAndTime() {
-        LastCommittedOffset offset = new LastCommittedOffset(500, 999L);
+  @Test
+  void toString_containsOffsetAndTime() {
+    LastCommittedOffset offset = new LastCommittedOffset(500, 999L);
 
-        String str = offset.toString();
+    String str = offset.toString();
 
-        assertThat(str).contains("500");
-        assertThat(str).contains("999");
-        assertThat(str).startsWith("LastCommittedOffset{");
-        assertThat(str).endsWith("}");
-    }
+    assertThat(str).contains("500");
+    assertThat(str).contains("999");
+    assertThat(str).startsWith("LastCommittedOffset{");
+    assertThat(str).endsWith("}");
+  }
 
-    @Test
-    void toString_format() {
-        LastCommittedOffset offset = new LastCommittedOffset(100, 200);
+  @Test
+  void toString_format() {
+    LastCommittedOffset offset = new LastCommittedOffset(100, 200);
 
-        assertThat(offset.toString()).isEqualTo("LastCommittedOffset{offset=100, commitTimeMillis=200}");
-    }
+    assertThat(offset.toString())
+        .isEqualTo("LastCommittedOffset{offset=100, commitTimeMillis=200}");
+  }
 
-    @Test
-    void negativeOffset() {
-        LastCommittedOffset offset = new LastCommittedOffset(-1, 100);
+  @Test
+  void negativeOffset() {
+    LastCommittedOffset offset = new LastCommittedOffset(-1, 100);
 
-        assertThat(offset.getOffset()).isEqualTo(-1);
-    }
+    assertThat(offset.getOffset()).isEqualTo(-1);
+  }
 
-    @Test
-    void largeOffset() {
-        LastCommittedOffset offset = new LastCommittedOffset(Long.MAX_VALUE, Long.MAX_VALUE);
+  @Test
+  void largeOffset() {
+    LastCommittedOffset offset = new LastCommittedOffset(Long.MAX_VALUE, Long.MAX_VALUE);
 
-        assertThat(offset.getOffset()).isEqualTo(Long.MAX_VALUE);
-        assertThat(offset.getCommitTimeMillis()).isEqualTo(Long.MAX_VALUE);
-    }
+    assertThat(offset.getOffset()).isEqualTo(Long.MAX_VALUE);
+    assertThat(offset.getCommitTimeMillis()).isEqualTo(Long.MAX_VALUE);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/processor/BaseCompactedOffsetsProcessorTest.java b/src/test/java/com/uber/ugroup/processor/BaseCompactedOffsetsProcessorTest.java
index 1b0d6c0..9f64b3d 100644
--- a/src/test/java/com/uber/ugroup/processor/BaseCompactedOffsetsProcessorTest.java
+++ b/src/test/java/com/uber/ugroup/processor/BaseCompactedOffsetsProcessorTest.java
@@ -15,12 +15,25 @@
  */
 package com.uber.ugroup.processor;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyDouble;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import com.uber.ugroup.fetcher.OffsetFetcher;
 import com.uber.ugroup.metrics.MetricsProvider;
 import com.uber.ugroup.model.ConsumerLagState;
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
 import com.uber.ugroup.watchlist.WatchListProvider;
+import java.util.List;
+import java.util.Map;
 import org.apache.kafka.clients.consumer.OffsetAndMetadata;
 import org.apache.kafka.common.Node;
 import org.apache.kafka.common.PartitionInfo;
@@ -31,328 +44,312 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
-import java.util.List;
-import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyDouble;
-import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @ExtendWith(MockitoExtension.class)
 class BaseCompactedOffsetsProcessorTest {
 
-    @Mock
-    private MetricsProvider metricsProvider;
+  @Mock private MetricsProvider metricsProvider;
+
+  @Mock private OffsetFetcher offsetFetcher;
 
-    @Mock
-    private OffsetFetcher offsetFetcher;
+  @Mock private WatchListProvider watchListProvider;
 
-    @Mock
-    private WatchListProvider watchListProvider;
+  private BaseCompactedOffsetsProcessor processor;
 
-    private BaseCompactedOffsetsProcessor processor;
+  private static final String CLUSTER = "test-cluster";
 
-    private static final String CLUSTER = "test-cluster";
+  @BeforeEach
+  void setUp() {
+    processor =
+        new BaseCompactedOffsetsProcessor(
+            metricsProvider, offsetFetcher, watchListProvider, CLUSTER);
+  }
 
-    @BeforeEach
-    void setUp() {
-        processor = new BaseCompactedOffsetsProcessor(metricsProvider, offsetFetcher, watchListProvider, CLUSTER);
-    }
+  @Test
+  void process_notInWatchList_returnsFalse() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(false);
 
-    @Test
-    void process_notInWatchList_returnsFalse() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(false);
+    boolean result = processor.process(gat, Map.of());
 
-        boolean result = processor.process(gat, Map.of());
+    assertThat(result).isFalse();
+    verify(offsetFetcher, never()).partitionsFor(anyString());
+  }
 
-        assertThat(result).isFalse();
-        verify(offsetFetcher, never()).partitionsFor(anyString());
-    }
+  @Test
+  void process_inWatchList_returnsTrue() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(watchListProvider.getName()).thenReturn("static");
 
-    @Test
-    void process_inWatchList_returnsTrue() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(watchListProvider.getName()).thenReturn("static");
+    Node node = new Node(0, "localhost", 9092);
+    List partitions =
+        List.of(new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node}));
+    when(offsetFetcher.partitionsFor("topic")).thenReturn(partitions);
 
-        Node node = new Node(0, "localhost", 9092);
-        List partitions = List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node})
-        );
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(partitions);
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
 
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(50, System.currentTimeMillis()));
 
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(50, System.currentTimeMillis())
-        );
+    boolean result = processor.process(gat, offsets);
 
-        boolean result = processor.process(gat, offsets);
+    assertThat(result).isTrue();
+  }
 
-        assertThat(result).isTrue();
-    }
+  @Test
+  void process_reportsLagMetrics() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(watchListProvider.getName()).thenReturn("static");
 
-    @Test
-    void process_reportsLagMetrics() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(watchListProvider.getName()).thenReturn("static");
+    Node node = new Node(0, "localhost", 9092);
+    List partitions =
+        List.of(new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node}));
+    when(offsetFetcher.partitionsFor("topic")).thenReturn(partitions);
 
-        Node node = new Node(0, "localhost", 9092);
-        List partitions = List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node})
-        );
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(partitions);
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
 
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(80, System.currentTimeMillis()));
 
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(80, System.currentTimeMillis())
-        );
+    processor.process(gat, offsets);
 
-        processor.process(gat, offsets);
+    // Verify lag gauge was recorded (endOffset - committedOffset = 100 - 80 = 20)
+    verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag"), eq(20.0), anyMap());
+    // Verify committed offset was recorded
+    verify(metricsProvider).recordGauge(eq("ugroup.committed_offset"), eq(80.0), anyMap());
+    // Verify percentage lag was recorded (20/100 * 100 = 20.0)
+    verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag_percentage"), eq(20.0), anyMap());
+  }
 
-        // Verify lag gauge was recorded (endOffset - committedOffset = 100 - 80 = 20)
-        verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag"), eq(20.0), anyMap());
-        // Verify committed offset was recorded
-        verify(metricsProvider).recordGauge(eq("ugroup.committed_offset"), eq(80.0), anyMap());
-        // Verify percentage lag was recorded (20/100 * 100 = 20.0)
-        verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag_percentage"), eq(20.0), anyMap());
-    }
+  @Test
+  void process_noPartitions_skips() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(offsetFetcher.partitionsFor("topic")).thenReturn(null);
 
-    @Test
-    void process_noPartitions_skips() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(null);
+    boolean result = processor.process(gat, Map.of());
 
-        boolean result = processor.process(gat, Map.of());
+    assertThat(result).isTrue();
+    verify(metricsProvider).incrementCounter(eq("ugroup.report.skipped"), anyMap());
+  }
 
-        assertThat(result).isTrue();
-        verify(metricsProvider).incrementCounter(eq("ugroup.report.skipped"), anyMap());
-    }
+  @Test
+  void process_emptyPartitions_skips() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(offsetFetcher.partitionsFor("topic")).thenReturn(List.of());
 
-    @Test
-    void process_emptyPartitions_skips() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(List.of());
+    boolean result = processor.process(gat, Map.of());
 
-        boolean result = processor.process(gat, Map.of());
-
-        assertThat(result).isTrue();
-        verify(metricsProvider).incrementCounter(eq("ugroup.report.skipped"), anyMap());
-    }
-
-    @Test
-    void process_emptyBeginningOffsets_skips() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-
-        Node node = new Node(0, "localhost", 9092);
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node})));
-
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of());
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
-
-        processor.process(gat, Map.of());
-
-        verify(metricsProvider).incrementCounter(eq("ugroup.report.skipped"), anyMap());
-    }
-
-    @Test
-    void process_emptyEndOffsets_skips() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-
-        Node node = new Node(0, "localhost", 9092);
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node})));
-
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of());
-
-        processor.process(gat, Map.of());
-
-        verify(metricsProvider).incrementCounter(eq("ugroup.report.skipped"), anyMap());
-    }
-
-    @Test
-    void process_missingPartitionOffset_fetchesFromBroker() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(watchListProvider.getName()).thenReturn("static");
-
-        Node node = new Node(0, "localhost", 9092);
-        List partitions = List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node}),
-                new PartitionInfo("topic", 1, node, new Node[]{node}, new Node[]{node})
-        );
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(partitions);
-
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        TopicPartition tp1 = new TopicPartition("topic", 1);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L, tp1, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L, tp1, 200L));
-
-        // Only provide offset for partition 0; partition 1 should be fetched from broker
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(50, System.currentTimeMillis())
-        );
-
-        when(offsetFetcher.listConsumerGroupOffsets("group"))
-                .thenReturn(Map.of(tp1, new OffsetAndMetadata(150L)));
-
-        processor.process(gat, offsets);
-
-        // Should have fetched group offsets from broker for the missing partition
-        verify(offsetFetcher).listConsumerGroupOffsets("group");
-        // Lag for partition 1 should be 200 - 150 = 50
-        verify(metricsProvider, atLeastOnce()).recordGauge(eq("ugroup.consumer_lag"), anyDouble(), anyMap());
-    }
-
-    @Test
-    void process_missingPartitionOffset_noCommittedOffset_usesNegativeOne() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(watchListProvider.getName()).thenReturn("static");
-
-        Node node = new Node(0, "localhost", 9092);
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node})));
-
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
-
-        // No committed offsets provided, and broker returns empty too
-        when(offsetFetcher.listConsumerGroupOffsets("group")).thenReturn(Map.of());
-
-        processor.process(gat, Map.of());
-
-        // Lag should be entire range (100 - 0 = 100) since committed is -1
-        verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag"), eq(100.0), anyMap());
-    }
-
-    @Test
-    void process_zeroLag_reportsZeroTimeLag() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(watchListProvider.getName()).thenReturn("static");
-
-        Node node = new Node(0, "localhost", 9092);
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node})));
-
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
-
-        // Committed offset equals end offset => zero lag
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(100, System.currentTimeMillis())
-        );
-
-        processor.process(gat, offsets);
-
-        verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag"), eq(0.0), anyMap());
-        verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag_seconds"), eq(0.0), anyMap());
-    }
-
-    @Test
-    void process_onException_returnsTrue_andIncrementsErrorCounter() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(offsetFetcher.partitionsFor("topic")).thenThrow(new RuntimeException("boom"));
-
-        boolean result = processor.process(gat, Map.of());
-
-        assertThat(result).isTrue();
-        verify(metricsProvider).incrementCounter(eq("ugroup.processor.errors"), anyMap());
-    }
-
-    @Test
-    void getLagState_afterProcess() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(watchListProvider.getName()).thenReturn("static");
-
-        Node node = new Node(0, "localhost", 9092);
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node})));
-
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
-
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(75, System.currentTimeMillis())
-        );
-
-        processor.process(gat, offsets);
-
-        ConsumerLagState lagState = processor.getLagState(gat);
-        assertThat(lagState).isNotNull();
-        assertThat(lagState.getGroupAndTopic()).isEqualTo(gat);
-        assertThat(lagState.getPartitionLagStates()).containsKey(0);
-
-        ConsumerLagState.PartitionLagState p0 = lagState.getPartitionLagStates().get(0);
-        assertThat(p0.getCommittedOffset()).isEqualTo(75);
-        assertThat(p0.getEndOffset()).isEqualTo(100);
-        assertThat(p0.getBeginningOffset()).isEqualTo(0);
-        assertThat(p0.getOffsetLag()).isEqualTo(25);
-    }
-
-    @Test
-    void getLagState_beforeProcess_returnsNull() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        assertThat(processor.getLagState(gat)).isNull();
-    }
-
-    @Test
-    void process_multiplePartitions_reportsEachPartition() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        when(watchListProvider.contains(gat)).thenReturn(true);
-        when(watchListProvider.getName()).thenReturn("static");
-
-        Node node = new Node(0, "localhost", 9092);
-        List partitions = List.of(
-                new PartitionInfo("topic", 0, node, new Node[]{node}, new Node[]{node}),
-                new PartitionInfo("topic", 1, node, new Node[]{node}, new Node[]{node})
-        );
-        when(offsetFetcher.partitionsFor("topic")).thenReturn(partitions);
-
-        TopicPartition tp0 = new TopicPartition("topic", 0);
-        TopicPartition tp1 = new TopicPartition("topic", 1);
-        when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L, tp1, 0L));
-        when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L, tp1, 200L));
-
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(90, System.currentTimeMillis()),
-                1, new LastCommittedOffset(150, System.currentTimeMillis())
-        );
-
-        processor.process(gat, offsets);
-
-        ConsumerLagState lagState = processor.getLagState(gat);
-        assertThat(lagState.getPartitionLagStates()).hasSize(2);
-        assertThat(lagState.getPartitionLagStates().get(0).getOffsetLag()).isEqualTo(10);
-        assertThat(lagState.getPartitionLagStates().get(1).getOffsetLag()).isEqualTo(50);
-        assertThat(lagState.getTotalOffsetLag()).isEqualTo(60);
-    }
+    assertThat(result).isTrue();
+    verify(metricsProvider).incrementCounter(eq("ugroup.report.skipped"), anyMap());
+  }
+
+  @Test
+  void process_emptyBeginningOffsets_skips() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+
+    Node node = new Node(0, "localhost", 9092);
+    when(offsetFetcher.partitionsFor("topic"))
+        .thenReturn(
+            List.of(new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node})));
+
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of());
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
+
+    processor.process(gat, Map.of());
+
+    verify(metricsProvider).incrementCounter(eq("ugroup.report.skipped"), anyMap());
+  }
+
+  @Test
+  void process_emptyEndOffsets_skips() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+
+    Node node = new Node(0, "localhost", 9092);
+    when(offsetFetcher.partitionsFor("topic"))
+        .thenReturn(
+            List.of(new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node})));
+
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of());
+
+    processor.process(gat, Map.of());
+
+    verify(metricsProvider).incrementCounter(eq("ugroup.report.skipped"), anyMap());
+  }
+
+  @Test
+  void process_missingPartitionOffset_fetchesFromBroker() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(watchListProvider.getName()).thenReturn("static");
+
+    Node node = new Node(0, "localhost", 9092);
+    List partitions =
+        List.of(
+            new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node}),
+            new PartitionInfo("topic", 1, node, new Node[] {node}, new Node[] {node}));
+    when(offsetFetcher.partitionsFor("topic")).thenReturn(partitions);
+
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    TopicPartition tp1 = new TopicPartition("topic", 1);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L, tp1, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L, tp1, 200L));
+
+    // Only provide offset for partition 0; partition 1 should be fetched from broker
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(50, System.currentTimeMillis()));
+
+    when(offsetFetcher.listConsumerGroupOffsets("group"))
+        .thenReturn(Map.of(tp1, new OffsetAndMetadata(150L)));
+
+    processor.process(gat, offsets);
+
+    // Should have fetched group offsets from broker for the missing partition
+    verify(offsetFetcher).listConsumerGroupOffsets("group");
+    // Lag for partition 1 should be 200 - 150 = 50
+    verify(metricsProvider, atLeastOnce())
+        .recordGauge(eq("ugroup.consumer_lag"), anyDouble(), anyMap());
+  }
+
+  @Test
+  void process_missingPartitionOffset_noCommittedOffset_usesNegativeOne() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(watchListProvider.getName()).thenReturn("static");
+
+    Node node = new Node(0, "localhost", 9092);
+    when(offsetFetcher.partitionsFor("topic"))
+        .thenReturn(
+            List.of(new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node})));
+
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
+
+    // No committed offsets provided, and broker returns empty too
+    when(offsetFetcher.listConsumerGroupOffsets("group")).thenReturn(Map.of());
+
+    processor.process(gat, Map.of());
+
+    // Lag should be entire range (100 - 0 = 100) since committed is -1
+    verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag"), eq(100.0), anyMap());
+  }
+
+  @Test
+  void process_zeroLag_reportsZeroTimeLag() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(watchListProvider.getName()).thenReturn("static");
+
+    Node node = new Node(0, "localhost", 9092);
+    when(offsetFetcher.partitionsFor("topic"))
+        .thenReturn(
+            List.of(new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node})));
+
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
+
+    // Committed offset equals end offset => zero lag
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(100, System.currentTimeMillis()));
+
+    processor.process(gat, offsets);
+
+    verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag"), eq(0.0), anyMap());
+    verify(metricsProvider).recordGauge(eq("ugroup.consumer_lag_seconds"), eq(0.0), anyMap());
+  }
+
+  @Test
+  void process_onException_returnsTrue_andIncrementsErrorCounter() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(offsetFetcher.partitionsFor("topic")).thenThrow(new RuntimeException("boom"));
+
+    boolean result = processor.process(gat, Map.of());
+
+    assertThat(result).isTrue();
+    verify(metricsProvider).incrementCounter(eq("ugroup.processor.errors"), anyMap());
+  }
+
+  @Test
+  void getLagState_afterProcess() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(watchListProvider.getName()).thenReturn("static");
+
+    Node node = new Node(0, "localhost", 9092);
+    when(offsetFetcher.partitionsFor("topic"))
+        .thenReturn(
+            List.of(new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node})));
+
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L));
+
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(75, System.currentTimeMillis()));
+
+    processor.process(gat, offsets);
+
+    ConsumerLagState lagState = processor.getLagState(gat);
+    assertThat(lagState).isNotNull();
+    assertThat(lagState.getGroupAndTopic()).isEqualTo(gat);
+    assertThat(lagState.getPartitionLagStates()).containsKey(0);
+
+    ConsumerLagState.PartitionLagState p0 = lagState.getPartitionLagStates().get(0);
+    assertThat(p0.getCommittedOffset()).isEqualTo(75);
+    assertThat(p0.getEndOffset()).isEqualTo(100);
+    assertThat(p0.getBeginningOffset()).isEqualTo(0);
+    assertThat(p0.getOffsetLag()).isEqualTo(25);
+  }
+
+  @Test
+  void getLagState_beforeProcess_returnsNull() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    assertThat(processor.getLagState(gat)).isNull();
+  }
+
+  @Test
+  void process_multiplePartitions_reportsEachPartition() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    when(watchListProvider.contains(gat)).thenReturn(true);
+    when(watchListProvider.getName()).thenReturn("static");
+
+    Node node = new Node(0, "localhost", 9092);
+    List partitions =
+        List.of(
+            new PartitionInfo("topic", 0, node, new Node[] {node}, new Node[] {node}),
+            new PartitionInfo("topic", 1, node, new Node[] {node}, new Node[] {node}));
+    when(offsetFetcher.partitionsFor("topic")).thenReturn(partitions);
+
+    TopicPartition tp0 = new TopicPartition("topic", 0);
+    TopicPartition tp1 = new TopicPartition("topic", 1);
+    when(offsetFetcher.beginningOffsets(any())).thenReturn(Map.of(tp0, 0L, tp1, 0L));
+    when(offsetFetcher.endOffsets(any())).thenReturn(Map.of(tp0, 100L, tp1, 200L));
+
+    Map offsets =
+        Map.of(
+            0, new LastCommittedOffset(90, System.currentTimeMillis()),
+            1, new LastCommittedOffset(150, System.currentTimeMillis()));
+
+    processor.process(gat, offsets);
+
+    ConsumerLagState lagState = processor.getLagState(gat);
+    assertThat(lagState.getPartitionLagStates()).hasSize(2);
+    assertThat(lagState.getPartitionLagStates().get(0).getOffsetLag()).isEqualTo(10);
+    assertThat(lagState.getPartitionLagStates().get(1).getOffsetLag()).isEqualTo(50);
+    assertThat(lagState.getTotalOffsetLag()).isEqualTo(60);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/processor/CommittedOffsetsCompactorTest.java b/src/test/java/com/uber/ugroup/processor/CommittedOffsetsCompactorTest.java
index f00475a..42f7858 100644
--- a/src/test/java/com/uber/ugroup/processor/CommittedOffsetsCompactorTest.java
+++ b/src/test/java/com/uber/ugroup/processor/CommittedOffsetsCompactorTest.java
@@ -15,12 +15,22 @@
  */
 package com.uber.ugroup.processor;
 
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
 import com.uber.ugroup.fetcher.OffsetFetcher;
 import com.uber.ugroup.metrics.MetricsProvider;
 import com.uber.ugroup.model.GroupAndTopic;
-import com.uber.ugroup.model.LastCommittedOffset;
 import com.uber.ugroup.watchlist.BlockList;
 import com.uber.ugroup.watchlist.WatchListProvider;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -28,239 +38,211 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyDouble;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @ExtendWith(MockitoExtension.class)
 class CommittedOffsetsCompactorTest {
 
-    @Mock
-    private MetricsProvider metricsProvider;
-
-    @Mock
-    private OffsetFetcher offsetFetcher;
-
-    @Mock
-    private CommittedOffsetsCompactor.CompactorEventCallback eventCallback;
-
-    private BlockList blockList;
-    private List watchListProviders;
-    private CommittedOffsetsCompactor compactor;
-
-    @BeforeEach
-    void setUp() {
-        blockList = BlockList.empty();
-        watchListProviders = new ArrayList<>();
-
-        // Use a very long compaction interval to prevent scheduled runs from interfering
-        compactor = new CommittedOffsetsCompactor(
-                metricsProvider,
-                offsetFetcher,
-                blockList,
-                watchListProviders,
-                eventCallback,
-                2,   // parallelism
-                50,  // consumerOffsetsPartitionCount
-                600_000, // compactIntervalMillis - long to avoid scheduled runs
-                0    // compactingDelayMillis - zero so items are ready immediately
-        );
-    }
-
-    @AfterEach
-    void tearDown() {
-        compactor.close();
-    }
-
-    @Test
-    void offsetCommitted_recordsOffset() {
-        compactor.offsetCommitted("my-group", "my-topic", 0, 100L);
-
-        // Verify metric was recorded (via LastCommittedOffsetState which calls metricsProvider)
-        // The state is internal, but we can verify the compactor doesn't throw
-        // and the offset is tracked by adding the group-topic to compact
-        GroupAndTopic gat = new GroupAndTopic("my-group", "my-topic");
-        compactor.addGroupAndTopicsToCompact(Set.of(gat), System.currentTimeMillis());
-
-        // Metrics should have been incremented for adding tasks
-        verify(metricsProvider).incrementCounter(
-                "ugroup.compactor.tasks_submitted", 1L, Map.of());
-    }
-
-    @Test
-    void addGroupAndTopicsToCompact_nullSet() {
-        compactor.addGroupAndTopicsToCompact(null, System.currentTimeMillis());
-
-        // Should not throw and should not increment counter
-        verify(metricsProvider, never()).incrementCounter(
-                anyString(), anyLong(), anyMap());
-    }
-
-    @Test
-    void addGroupAndTopicsToCompact_emptySet() {
-        compactor.addGroupAndTopicsToCompact(Set.of(), System.currentTimeMillis());
-
-        verify(metricsProvider).incrementCounter(
-                "ugroup.compactor.tasks_submitted", 0L, Map.of());
-    }
-
-    @Test
-    void addGroupAndTopicsToCompact_filtersBlockedEntries() {
-        blockList.blockGroup("blocked-group");
-
-        GroupAndTopic allowed = new GroupAndTopic("allowed-group", "topic");
-        GroupAndTopic blocked = new GroupAndTopic("blocked-group", "topic");
-
-        compactor.addGroupAndTopicsToCompact(Set.of(allowed, blocked), System.currentTimeMillis());
-
-        // Both are submitted to the counter, but only the non-blocked one enters the pipeline
-        verify(metricsProvider).incrementCounter(
-                "ugroup.compactor.tasks_submitted", 2L, Map.of());
-    }
-
-    @Test
-    void addGroupAndTopicsToCompact_deduplicates() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        long firstTimestamp = 1000L;
-        long secondTimestamp = 2000L;
-
-        compactor.addGroupAndTopicsToCompact(Set.of(gat), firstTimestamp);
-        compactor.addGroupAndTopicsToCompact(Set.of(gat), secondTimestamp);
-
-        // putIfAbsent means the first timestamp wins; both calls increment the counter
-        verify(metricsProvider, org.mockito.Mockito.atLeast(2)).incrementCounter(
-                "ugroup.compactor.tasks_submitted", 1L, Map.of());
-    }
-
-    @Test
-    void updatePartitionAssigned_updatesPartitions() {
-        Set partitions = Set.of(0, 1, 2);
-        compactor.updatePartitionAssigned(partitions);
-
-        // Should not throw. The method updates internal state and invalidates
-        // unassigned entries in LastCommittedOffsetState.
-    }
-
-    @Test
-    void updatePartitionAssigned_emptySet() {
-        compactor.updatePartitionAssigned(Set.of());
-
-        // Should handle empty partition set without error
-    }
-
-    @Test
-    void updatePartitionAssigned_clearsPreviousState() {
-        // Record some offsets
-        compactor.offsetCommitted("group-a", "topic-1", 0, 100L);
-        compactor.offsetCommitted("group-b", "topic-2", 0, 200L);
-
-        // Assign all partitions, then reassign to empty
-        Set allPartitions = new HashSet<>();
-        for (int i = 0; i < 50; i++) {
-            allPartitions.add(i);
-        }
-        compactor.updatePartitionAssigned(allPartitions);
-
-        // Now assign empty set - should invalidate all entries
-        compactor.updatePartitionAssigned(Set.of());
-    }
-
-    @Test
-    void close_shutsDownExecutors() {
-        // Create a separate compactor so we can close it independently
-        CommittedOffsetsCompactor c = new CommittedOffsetsCompactor(
-                metricsProvider,
-                offsetFetcher,
-                blockList,
-                watchListProviders,
-                eventCallback,
-                1,
-                50,
-                600_000,
-                0
-        );
-
-        // Close should not throw
-        c.close();
-    }
-
-    @Test
-    void close_handlesMultipleCalls() {
-        CommittedOffsetsCompactor c = new CommittedOffsetsCompactor(
-                metricsProvider,
-                offsetFetcher,
-                blockList,
-                watchListProviders,
-                eventCallback,
-                1,
-                50,
-                600_000,
-                0
-        );
-
-        c.close();
-        // Second close should not throw
-        c.close();
-    }
-
-    @Test
-    void multipleOffsetCommits_samePartition() {
-        compactor.offsetCommitted("group", "topic", 0, 100L);
-        compactor.offsetCommitted("group", "topic", 0, 200L);
-
-        // The second commit should overwrite the first in LastCommittedOffsetState
-        // No errors should occur
-    }
-
-    @Test
-    void multipleOffsetCommits_differentPartitions() {
-        compactor.offsetCommitted("group", "topic", 0, 100L);
-        compactor.offsetCommitted("group", "topic", 1, 200L);
-        compactor.offsetCommitted("group", "topic", 2, 300L);
-
-        // All three partitions should be tracked
-    }
-
-    @Test
-    void addGroupAndTopicsToCompact_multipleGroupsAndTopics() {
-        Set gats = new HashSet<>();
-        gats.add(new GroupAndTopic("group-1", "topic-a"));
-        gats.add(new GroupAndTopic("group-1", "topic-b"));
-        gats.add(new GroupAndTopic("group-2", "topic-a"));
-
-        compactor.addGroupAndTopicsToCompact(gats, System.currentTimeMillis());
-
-        verify(metricsProvider).incrementCounter(
-                "ugroup.compactor.tasks_submitted", 3L, Map.of());
-    }
-
-    @Test
-    void updatePartitionAssigned_defensiveCopy() {
-        Set partitions = new HashSet<>(Set.of(0, 1, 2));
-        compactor.updatePartitionAssigned(partitions);
-
-        // Modifying the original set after passing should not affect internal state
-        partitions.add(3);
-        partitions.add(4);
-
-        // No way to directly verify the internal state, but the method should
-        // have taken a defensive copy
-    }
+  @Mock private MetricsProvider metricsProvider;
+
+  @Mock private OffsetFetcher offsetFetcher;
+
+  @Mock private CommittedOffsetsCompactor.CompactorEventCallback eventCallback;
+
+  private BlockList blockList;
+  private List watchListProviders;
+  private CommittedOffsetsCompactor compactor;
+
+  @BeforeEach
+  void setUp() {
+    blockList = BlockList.empty();
+    watchListProviders = new ArrayList<>();
+
+    // Use a very long compaction interval to prevent scheduled runs from interfering
+    compactor =
+        new CommittedOffsetsCompactor(
+            metricsProvider,
+            offsetFetcher,
+            blockList,
+            watchListProviders,
+            eventCallback,
+            2, // parallelism
+            50, // consumerOffsetsPartitionCount
+            600_000, // compactIntervalMillis - long to avoid scheduled runs
+            0 // compactingDelayMillis - zero so items are ready immediately
+            );
+  }
+
+  @AfterEach
+  void tearDown() {
+    compactor.close();
+  }
+
+  @Test
+  void offsetCommitted_recordsOffset() {
+    compactor.offsetCommitted("my-group", "my-topic", 0, 100L);
+
+    // Verify metric was recorded (via LastCommittedOffsetState which calls metricsProvider)
+    // The state is internal, but we can verify the compactor doesn't throw
+    // and the offset is tracked by adding the group-topic to compact
+    GroupAndTopic gat = new GroupAndTopic("my-group", "my-topic");
+    compactor.addGroupAndTopicsToCompact(Set.of(gat), System.currentTimeMillis());
+
+    // Metrics should have been incremented for adding tasks
+    verify(metricsProvider).incrementCounter("ugroup.compactor.tasks_submitted", 1L, Map.of());
+  }
+
+  @Test
+  void addGroupAndTopicsToCompact_nullSet() {
+    compactor.addGroupAndTopicsToCompact(null, System.currentTimeMillis());
+
+    // Should not throw and should not increment counter
+    verify(metricsProvider, never()).incrementCounter(anyString(), anyLong(), anyMap());
+  }
+
+  @Test
+  void addGroupAndTopicsToCompact_emptySet() {
+    compactor.addGroupAndTopicsToCompact(Set.of(), System.currentTimeMillis());
+
+    verify(metricsProvider).incrementCounter("ugroup.compactor.tasks_submitted", 0L, Map.of());
+  }
+
+  @Test
+  void addGroupAndTopicsToCompact_filtersBlockedEntries() {
+    blockList.blockGroup("blocked-group");
+
+    GroupAndTopic allowed = new GroupAndTopic("allowed-group", "topic");
+    GroupAndTopic blocked = new GroupAndTopic("blocked-group", "topic");
+
+    compactor.addGroupAndTopicsToCompact(Set.of(allowed, blocked), System.currentTimeMillis());
+
+    // Both are submitted to the counter, but only the non-blocked one enters the pipeline
+    verify(metricsProvider).incrementCounter("ugroup.compactor.tasks_submitted", 2L, Map.of());
+  }
+
+  @Test
+  void addGroupAndTopicsToCompact_deduplicates() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    long firstTimestamp = 1000L;
+    long secondTimestamp = 2000L;
+
+    compactor.addGroupAndTopicsToCompact(Set.of(gat), firstTimestamp);
+    compactor.addGroupAndTopicsToCompact(Set.of(gat), secondTimestamp);
+
+    // putIfAbsent means the first timestamp wins; both calls increment the counter
+    verify(metricsProvider, org.mockito.Mockito.atLeast(2))
+        .incrementCounter("ugroup.compactor.tasks_submitted", 1L, Map.of());
+  }
+
+  @Test
+  void updatePartitionAssigned_updatesPartitions() {
+    Set partitions = Set.of(0, 1, 2);
+    compactor.updatePartitionAssigned(partitions);
+
+    // Should not throw. The method updates internal state and invalidates
+    // unassigned entries in LastCommittedOffsetState.
+  }
+
+  @Test
+  void updatePartitionAssigned_emptySet() {
+    compactor.updatePartitionAssigned(Set.of());
+
+    // Should handle empty partition set without error
+  }
+
+  @Test
+  void updatePartitionAssigned_clearsPreviousState() {
+    // Record some offsets
+    compactor.offsetCommitted("group-a", "topic-1", 0, 100L);
+    compactor.offsetCommitted("group-b", "topic-2", 0, 200L);
+
+    // Assign all partitions, then reassign to empty
+    Set allPartitions = new HashSet<>();
+    for (int i = 0; i < 50; i++) {
+      allPartitions.add(i);
+    }
+    compactor.updatePartitionAssigned(allPartitions);
+
+    // Now assign empty set - should invalidate all entries
+    compactor.updatePartitionAssigned(Set.of());
+  }
+
+  @Test
+  void close_shutsDownExecutors() {
+    // Create a separate compactor so we can close it independently
+    CommittedOffsetsCompactor c =
+        new CommittedOffsetsCompactor(
+            metricsProvider,
+            offsetFetcher,
+            blockList,
+            watchListProviders,
+            eventCallback,
+            1,
+            50,
+            600_000,
+            0);
+
+    // Close should not throw
+    c.close();
+  }
+
+  @Test
+  void close_handlesMultipleCalls() {
+    CommittedOffsetsCompactor c =
+        new CommittedOffsetsCompactor(
+            metricsProvider,
+            offsetFetcher,
+            blockList,
+            watchListProviders,
+            eventCallback,
+            1,
+            50,
+            600_000,
+            0);
+
+    c.close();
+    // Second close should not throw
+    c.close();
+  }
+
+  @Test
+  void multipleOffsetCommits_samePartition() {
+    compactor.offsetCommitted("group", "topic", 0, 100L);
+    compactor.offsetCommitted("group", "topic", 0, 200L);
+
+    // The second commit should overwrite the first in LastCommittedOffsetState
+    // No errors should occur
+  }
+
+  @Test
+  void multipleOffsetCommits_differentPartitions() {
+    compactor.offsetCommitted("group", "topic", 0, 100L);
+    compactor.offsetCommitted("group", "topic", 1, 200L);
+    compactor.offsetCommitted("group", "topic", 2, 300L);
+
+    // All three partitions should be tracked
+  }
+
+  @Test
+  void addGroupAndTopicsToCompact_multipleGroupsAndTopics() {
+    Set gats = new HashSet<>();
+    gats.add(new GroupAndTopic("group-1", "topic-a"));
+    gats.add(new GroupAndTopic("group-1", "topic-b"));
+    gats.add(new GroupAndTopic("group-2", "topic-a"));
+
+    compactor.addGroupAndTopicsToCompact(gats, System.currentTimeMillis());
+
+    verify(metricsProvider).incrementCounter("ugroup.compactor.tasks_submitted", 3L, Map.of());
+  }
+
+  @Test
+  void updatePartitionAssigned_defensiveCopy() {
+    Set partitions = new HashSet<>(Set.of(0, 1, 2));
+    compactor.updatePartitionAssigned(partitions);
+
+    // Modifying the original set after passing should not affect internal state
+    partitions.add(3);
+    partitions.add(4);
+
+    // No way to directly verify the internal state, but the method should
+    // have taken a defensive copy
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/processor/ConsumerOffsetTopicProcessorTest.java b/src/test/java/com/uber/ugroup/processor/ConsumerOffsetTopicProcessorTest.java
index a85acc0..3784633 100644
--- a/src/test/java/com/uber/ugroup/processor/ConsumerOffsetTopicProcessorTest.java
+++ b/src/test/java/com/uber/ugroup/processor/ConsumerOffsetTopicProcessorTest.java
@@ -15,13 +15,28 @@
  */
 package com.uber.ugroup.processor;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyDouble;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import com.uber.ugroup.config.UGroupProperties;
 import com.uber.ugroup.fetcher.OffsetFetcher;
 import com.uber.ugroup.metrics.MetricsProvider;
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
 import com.uber.ugroup.watchlist.BlockList;
-import com.uber.ugroup.watchlist.WatchListProvider;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import org.apache.kafka.clients.consumer.ConsumerRecords;
 import org.apache.kafka.clients.consumer.KafkaConsumer;
 import org.apache.kafka.common.TopicPartition;
@@ -32,327 +47,301 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
-import java.time.Duration;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyDouble;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @ExtendWith(MockitoExtension.class)
 class ConsumerOffsetTopicProcessorTest {
 
-    @Mock
-    private MetricsProvider metricsProvider;
-
-    @Mock
-    private KafkaConsumer consumer;
-
-    @Mock
-    private OffsetFetcher offsetFetcher;
-
-    @Mock
-    private CompactedOffsetsProcessor compactedOffsetsProcessor;
-
-    private UGroupProperties properties;
-    private BlockList blockList;
-    private ConsumerOffsetTopicProcessor processor;
-
-    @BeforeEach
-    void setUp() {
-        properties = new UGroupProperties();
-        properties.getKafka().setConsumerGroup("test-ugroup");
-        properties.getProcessing().setParallelism(1);
-        properties.getProcessing().setConsumerOffsetsPartitionCount(50);
-        properties.getProcessing().setCompactionIntervalMs(600_000);
-        properties.getProcessing().setLagReportIntervalMs(600_000);
-
-        blockList = BlockList.empty();
-
-        processor = new ConsumerOffsetTopicProcessor(
-                metricsProvider,
-                properties,
-                consumer,
-                List.of(compactedOffsetsProcessor),
-                offsetFetcher,
-                blockList,
-                Collections.emptyList()
-        );
-    }
-
-    @AfterEach
-    void tearDown() {
-        processor.close();
-    }
-
-    @Test
-    void stop_setsRunningToFalse() {
-        processor.stop();
-
-        // After stop, close should complete quickly since the latch
-        // won't be counted down (run() never started), but stop itself
-        // should not throw
-    }
-
-    @Test
-    void close_stopsProcessor() {
-        // close() calls stop() internally
-        processor.close();
-
-        // Should not throw, and should set running to false
-    }
-
-    @Test
-    void close_multipleCallsSafe() {
-        processor.close();
-        processor.close();
-
-        // Multiple close calls should not throw
-    }
-
-    @Test
-    void processCompactedOffset_delegatesToProcessors() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(100, System.currentTimeMillis()));
-
-        when(compactedOffsetsProcessor.process(gat, offsets)).thenReturn(true);
-
-        processor.processCompactedOffset(gat, offsets, System.currentTimeMillis());
-
-        verify(compactedOffsetsProcessor).process(gat, offsets);
-    }
-
-    @Test
-    void processCompactedOffset_skipsWhenEnqueueDelayExceeded() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(100, System.currentTimeMillis()));
-
-        // Enqueue timestamp far in the past (more than MAX_ENQUEUE_DELAY_MS = 5000)
-        long staleTimestamp = System.currentTimeMillis() - 10_000;
-
-        processor.processCompactedOffset(gat, offsets, staleTimestamp);
-
-        verify(compactedOffsetsProcessor, never()).process(any(), any());
-        verify(metricsProvider).incrementCounter("ugroup.processor.skipped_delayed", Map.of());
-    }
-
-    @Test
-    void processCompactedOffset_triesNextProcessor_whenFirstReturnsFalse() {
-        CompactedOffsetsProcessor secondProcessor = org.mockito.Mockito.mock(CompactedOffsetsProcessor.class);
-
-        // Recreate processor with two compacted offset processors
-        processor.close();
-        processor = new ConsumerOffsetTopicProcessor(
-                metricsProvider,
-                properties,
-                consumer,
-                List.of(compactedOffsetsProcessor, secondProcessor),
-                offsetFetcher,
-                blockList,
-                Collections.emptyList()
-        );
-
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(100, System.currentTimeMillis()));
-
-        when(compactedOffsetsProcessor.process(gat, offsets)).thenReturn(false);
-        when(secondProcessor.process(gat, offsets)).thenReturn(true);
-
-        processor.processCompactedOffset(gat, offsets, System.currentTimeMillis());
-
-        verify(compactedOffsetsProcessor).process(gat, offsets);
-        verify(secondProcessor).process(gat, offsets);
-    }
-
-    @Test
-    void processCompactedOffset_stopsAtFirstSuccessfulProcessor() {
-        CompactedOffsetsProcessor secondProcessor = org.mockito.Mockito.mock(CompactedOffsetsProcessor.class);
-
-        processor.close();
-        processor = new ConsumerOffsetTopicProcessor(
-                metricsProvider,
-                properties,
-                consumer,
-                List.of(compactedOffsetsProcessor, secondProcessor),
-                offsetFetcher,
-                blockList,
-                Collections.emptyList()
-        );
+  @Mock private MetricsProvider metricsProvider;
+
+  @Mock private KafkaConsumer consumer;
+
+  @Mock private OffsetFetcher offsetFetcher;
+
+  @Mock private CompactedOffsetsProcessor compactedOffsetsProcessor;
+
+  private UGroupProperties properties;
+  private BlockList blockList;
+  private ConsumerOffsetTopicProcessor processor;
+
+  @BeforeEach
+  void setUp() {
+    properties = new UGroupProperties();
+    properties.getKafka().setConsumerGroup("test-ugroup");
+    properties.getProcessing().setParallelism(1);
+    properties.getProcessing().setConsumerOffsetsPartitionCount(50);
+    properties.getProcessing().setCompactionIntervalMs(600_000);
+    properties.getProcessing().setLagReportIntervalMs(600_000);
+
+    blockList = BlockList.empty();
+
+    processor =
+        new ConsumerOffsetTopicProcessor(
+            metricsProvider,
+            properties,
+            consumer,
+            List.of(compactedOffsetsProcessor),
+            offsetFetcher,
+            blockList,
+            Collections.emptyList());
+  }
+
+  @AfterEach
+  void tearDown() {
+    processor.close();
+  }
+
+  @Test
+  void stop_setsRunningToFalse() {
+    processor.stop();
+
+    // After stop, close should complete quickly since the latch
+    // won't be counted down (run() never started), but stop itself
+    // should not throw
+  }
+
+  @Test
+  void close_stopsProcessor() {
+    // close() calls stop() internally
+    processor.close();
+
+    // Should not throw, and should set running to false
+  }
+
+  @Test
+  void close_multipleCallsSafe() {
+    processor.close();
+    processor.close();
+
+    // Multiple close calls should not throw
+  }
+
+  @Test
+  void processCompactedOffset_delegatesToProcessors() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(100, System.currentTimeMillis()));
+
+    when(compactedOffsetsProcessor.process(gat, offsets)).thenReturn(true);
+
+    processor.processCompactedOffset(gat, offsets, System.currentTimeMillis());
+
+    verify(compactedOffsetsProcessor).process(gat, offsets);
+  }
+
+  @Test
+  void processCompactedOffset_skipsWhenEnqueueDelayExceeded() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(100, System.currentTimeMillis()));
+
+    // Enqueue timestamp far in the past (more than MAX_ENQUEUE_DELAY_MS = 5000)
+    long staleTimestamp = System.currentTimeMillis() - 10_000;
+
+    processor.processCompactedOffset(gat, offsets, staleTimestamp);
+
+    verify(compactedOffsetsProcessor, never()).process(any(), any());
+    verify(metricsProvider).incrementCounter("ugroup.processor.skipped_delayed", Map.of());
+  }
+
+  @Test
+  void processCompactedOffset_triesNextProcessor_whenFirstReturnsFalse() {
+    CompactedOffsetsProcessor secondProcessor =
+        org.mockito.Mockito.mock(CompactedOffsetsProcessor.class);
+
+    // Recreate processor with two compacted offset processors
+    processor.close();
+    processor =
+        new ConsumerOffsetTopicProcessor(
+            metricsProvider,
+            properties,
+            consumer,
+            List.of(compactedOffsetsProcessor, secondProcessor),
+            offsetFetcher,
+            blockList,
+            Collections.emptyList());
+
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(100, System.currentTimeMillis()));
+
+    when(compactedOffsetsProcessor.process(gat, offsets)).thenReturn(false);
+    when(secondProcessor.process(gat, offsets)).thenReturn(true);
+
+    processor.processCompactedOffset(gat, offsets, System.currentTimeMillis());
+
+    verify(compactedOffsetsProcessor).process(gat, offsets);
+    verify(secondProcessor).process(gat, offsets);
+  }
+
+  @Test
+  void processCompactedOffset_stopsAtFirstSuccessfulProcessor() {
+    CompactedOffsetsProcessor secondProcessor =
+        org.mockito.Mockito.mock(CompactedOffsetsProcessor.class);
+
+    processor.close();
+    processor =
+        new ConsumerOffsetTopicProcessor(
+            metricsProvider,
+            properties,
+            consumer,
+            List.of(compactedOffsetsProcessor, secondProcessor),
+            offsetFetcher,
+            blockList,
+            Collections.emptyList());
 
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(100, System.currentTimeMillis()));
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(100, System.currentTimeMillis()));
 
-        when(compactedOffsetsProcessor.process(gat, offsets)).thenReturn(true);
+    when(compactedOffsetsProcessor.process(gat, offsets)).thenReturn(true);
 
-        processor.processCompactedOffset(gat, offsets, System.currentTimeMillis());
+    processor.processCompactedOffset(gat, offsets, System.currentTimeMillis());
 
-        verify(compactedOffsetsProcessor).process(gat, offsets);
-        verify(secondProcessor, never()).process(any(), any());
-    }
+    verify(compactedOffsetsProcessor).process(gat, offsets);
+    verify(secondProcessor, never()).process(any(), any());
+  }
 
-    @Test
-    void processCompactedOffset_recordsEnqueueDelay() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Map offsets = Map.of(
-                0, new LastCommittedOffset(100, System.currentTimeMillis()));
+  @Test
+  void processCompactedOffset_recordsEnqueueDelay() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Map offsets =
+        Map.of(0, new LastCommittedOffset(100, System.currentTimeMillis()));
 
-        when(compactedOffsetsProcessor.process(gat, offsets)).thenReturn(true);
+    when(compactedOffsetsProcessor.process(gat, offsets)).thenReturn(true);
 
-        processor.processCompactedOffset(gat, offsets, System.currentTimeMillis());
+    processor.processCompactedOffset(gat, offsets, System.currentTimeMillis());
 
-        verify(metricsProvider).recordGauge(
-                anyString(), anyDouble(), anyMap());
-    }
+    verify(metricsProvider).recordGauge(anyString(), anyDouble(), anyMap());
+  }
 
-    @Test
-    void processRecords_handlesEmptyRecords() {
-        ConsumerRecords emptyRecords = ConsumerRecords.empty();
+  @Test
+  void processRecords_handlesEmptyRecords() {
+    ConsumerRecords emptyRecords = ConsumerRecords.empty();
 
-        processor.processRecords(emptyRecords, System.currentTimeMillis());
+    processor.processRecords(emptyRecords, System.currentTimeMillis());
 
-        // No messages processed, no errors
-        verify(metricsProvider, never()).incrementCounter("ugroup.messages.processed", Map.of());
-    }
+    // No messages processed, no errors
+    verify(metricsProvider, never()).incrementCounter("ugroup.messages.processed", Map.of());
+  }
 
-    @Test
-    void reportConsumerOffsetConsumerLag_skipsWhenNotTime() {
-        // reportTime is far in the future
-        long futureReportTime = System.currentTimeMillis() + 600_000;
+  @Test
+  void reportConsumerOffsetConsumerLag_skipsWhenNotTime() {
+    // reportTime is far in the future
+    long futureReportTime = System.currentTimeMillis() + 600_000;
 
-        long result = processor.reportConsumerOffsetConsumerLag(futureReportTime);
+    long result = processor.reportConsumerOffsetConsumerLag(futureReportTime);
 
-        // Should return the same reportTime unchanged
-        assertThat(result).isEqualTo(futureReportTime);
-        // Should not call consumer.assignment() since it's not time to report
-        verify(consumer, never()).assignment();
-    }
+    // Should return the same reportTime unchanged
+    assertThat(result).isEqualTo(futureReportTime);
+    // Should not call consumer.assignment() since it's not time to report
+    verify(consumer, never()).assignment();
+  }
 
-    @Test
-    void reportConsumerOffsetConsumerLag_reportsWhenTimeReached() {
-        long pastReportTime = System.currentTimeMillis() - 1000;
+  @Test
+  void reportConsumerOffsetConsumerLag_reportsWhenTimeReached() {
+    long pastReportTime = System.currentTimeMillis() - 1000;
 
-        Set assignment = Set.of(
-                new TopicPartition("__consumer_offsets", 0));
-        when(consumer.assignment()).thenReturn(assignment);
-        when(consumer.endOffsets(assignment)).thenReturn(
-                Map.of(new TopicPartition("__consumer_offsets", 0), 1000L));
-        when(consumer.position(any(TopicPartition.class))).thenReturn(900L);
+    Set assignment = Set.of(new TopicPartition("__consumer_offsets", 0));
+    when(consumer.assignment()).thenReturn(assignment);
+    when(consumer.endOffsets(assignment))
+        .thenReturn(Map.of(new TopicPartition("__consumer_offsets", 0), 1000L));
+    when(consumer.position(any(TopicPartition.class))).thenReturn(900L);
 
-        long result = processor.reportConsumerOffsetConsumerLag(pastReportTime);
+    long result = processor.reportConsumerOffsetConsumerLag(pastReportTime);
 
-        // Should return a new future report time
-        assertThat(result).isGreaterThan(pastReportTime);
+    // Should return a new future report time
+    assertThat(result).isGreaterThan(pastReportTime);
 
-        verify(metricsProvider).recordGauge(
-                "ugroup.self_lag", 100.0,
-                Map.of("partition", "0"));
-    }
+    verify(metricsProvider).recordGauge("ugroup.self_lag", 100.0, Map.of("partition", "0"));
+  }
 
-    @Test
-    void reportConsumerOffsetConsumerLag_handlesEmptyAssignment() {
-        long pastReportTime = System.currentTimeMillis() - 1000;
+  @Test
+  void reportConsumerOffsetConsumerLag_handlesEmptyAssignment() {
+    long pastReportTime = System.currentTimeMillis() - 1000;
 
-        when(consumer.assignment()).thenReturn(Collections.emptySet());
-        when(consumer.endOffsets(Collections.emptySet())).thenReturn(Collections.emptyMap());
+    when(consumer.assignment()).thenReturn(Collections.emptySet());
+    when(consumer.endOffsets(Collections.emptySet())).thenReturn(Collections.emptyMap());
 
-        long result = processor.reportConsumerOffsetConsumerLag(pastReportTime);
+    long result = processor.reportConsumerOffsetConsumerLag(pastReportTime);
 
-        assertThat(result).isGreaterThan(pastReportTime);
-    }
+    assertThat(result).isGreaterThan(pastReportTime);
+  }
 
-    @Test
-    void reportConsumerOffsetConsumerLag_handlesException() {
-        long pastReportTime = System.currentTimeMillis() - 1000;
+  @Test
+  void reportConsumerOffsetConsumerLag_handlesException() {
+    long pastReportTime = System.currentTimeMillis() - 1000;
 
-        when(consumer.assignment()).thenThrow(new RuntimeException("Kafka error"));
+    when(consumer.assignment()).thenThrow(new RuntimeException("Kafka error"));
 
-        long result = processor.reportConsumerOffsetConsumerLag(pastReportTime);
+    long result = processor.reportConsumerOffsetConsumerLag(pastReportTime);
 
-        // Should still return a new report time despite the error
-        assertThat(result).isGreaterThan(pastReportTime);
-    }
+    // Should still return a new report time despite the error
+    assertThat(result).isGreaterThan(pastReportTime);
+  }
 
-    @Test
-    void reportConsumerOffsetConsumerLag_lagNeverNegative() {
-        long pastReportTime = System.currentTimeMillis() - 1000;
+  @Test
+  void reportConsumerOffsetConsumerLag_lagNeverNegative() {
+    long pastReportTime = System.currentTimeMillis() - 1000;
 
-        Set assignment = Set.of(
-                new TopicPartition("__consumer_offsets", 0));
-        when(consumer.assignment()).thenReturn(assignment);
-        // position > endOffset (edge case)
-        when(consumer.endOffsets(assignment)).thenReturn(
-                Map.of(new TopicPartition("__consumer_offsets", 0), 100L));
-        when(consumer.position(any(TopicPartition.class))).thenReturn(200L);
+    Set assignment = Set.of(new TopicPartition("__consumer_offsets", 0));
+    when(consumer.assignment()).thenReturn(assignment);
+    // position > endOffset (edge case)
+    when(consumer.endOffsets(assignment))
+        .thenReturn(Map.of(new TopicPartition("__consumer_offsets", 0), 100L));
+    when(consumer.position(any(TopicPartition.class))).thenReturn(200L);
 
-        processor.reportConsumerOffsetConsumerLag(pastReportTime);
+    processor.reportConsumerOffsetConsumerLag(pastReportTime);
 
-        // Lag should be max(0, endOffset - position) = 0
-        verify(metricsProvider).recordGauge(
-                "ugroup.self_lag", 0.0,
-                Map.of("partition", "0"));
-    }
+    // Lag should be max(0, endOffset - position) = 0
+    verify(metricsProvider).recordGauge("ugroup.self_lag", 0.0, Map.of("partition", "0"));
+  }
 
-    @Test
-    void processingLoop_pollsConsumer() {
-        when(consumer.poll(any(Duration.class))).thenReturn(ConsumerRecords.empty());
-        when(consumer.assignment()).thenReturn(Collections.emptySet());
+  @Test
+  void processingLoop_pollsConsumer() {
+    when(consumer.poll(any(Duration.class))).thenReturn(ConsumerRecords.empty());
+    when(consumer.assignment()).thenReturn(Collections.emptySet());
 
-        processor.processingLoop(System.currentTimeMillis() + 600_000);
+    processor.processingLoop(System.currentTimeMillis() + 600_000);
 
-        verify(consumer).poll(any(Duration.class));
-    }
+    verify(consumer).poll(any(Duration.class));
+  }
 
-    @Test
-    void processingLoop_tracksPartitionAssignment() {
-        when(consumer.poll(any(Duration.class))).thenReturn(ConsumerRecords.empty());
+  @Test
+  void processingLoop_tracksPartitionAssignment() {
+    when(consumer.poll(any(Duration.class))).thenReturn(ConsumerRecords.empty());
 
-        Set assignment = Set.of(
-                new TopicPartition("__consumer_offsets", 0),
-                new TopicPartition("__consumer_offsets", 1));
-        when(consumer.assignment()).thenReturn(assignment);
+    Set assignment =
+        Set.of(
+            new TopicPartition("__consumer_offsets", 0),
+            new TopicPartition("__consumer_offsets", 1));
+    when(consumer.assignment()).thenReturn(assignment);
 
-        processor.processingLoop(System.currentTimeMillis() + 600_000);
+    processor.processingLoop(System.currentTimeMillis() + 600_000);
 
-        verify(consumer).assignment();
-    }
+    verify(consumer).assignment();
+  }
 
-    @Test
-    void run_stopsWhenRunningSetToFalse() throws InterruptedException {
-        when(consumer.poll(any(Duration.class))).thenReturn(ConsumerRecords.empty());
-        when(consumer.assignment()).thenReturn(Collections.emptySet());
+  @Test
+  void run_stopsWhenRunningSetToFalse() throws InterruptedException {
+    when(consumer.poll(any(Duration.class))).thenReturn(ConsumerRecords.empty());
+    when(consumer.assignment()).thenReturn(Collections.emptySet());
 
-        CountDownLatch started = new CountDownLatch(1);
-        Thread thread = new Thread(() -> {
-            started.countDown();
-            processor.run();
-        });
-        thread.start();
+    CountDownLatch started = new CountDownLatch(1);
+    Thread thread =
+        new Thread(
+            () -> {
+              started.countDown();
+              processor.run();
+            });
+    thread.start();
 
-        assertThat(started.await(2, TimeUnit.SECONDS)).isTrue();
+    assertThat(started.await(2, TimeUnit.SECONDS)).isTrue();
 
-        // Give the loop a moment to start
-        Thread.sleep(100);
+    // Give the loop a moment to start
+    Thread.sleep(100);
 
-        processor.stop();
-        thread.join(5_000);
+    processor.stop();
+    thread.join(5_000);
 
-        assertThat(thread.isAlive()).isFalse();
-    }
+    assertThat(thread.isAlive()).isFalse();
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/processor/LagCalculationUtilsTest.java b/src/test/java/com/uber/ugroup/processor/LagCalculationUtilsTest.java
index 92b4e27..cd19b7b 100644
--- a/src/test/java/com/uber/ugroup/processor/LagCalculationUtilsTest.java
+++ b/src/test/java/com/uber/ugroup/processor/LagCalculationUtilsTest.java
@@ -15,77 +15,77 @@
  */
 package com.uber.ugroup.processor;
 
-import org.junit.jupiter.api.Test;
-
 import static org.assertj.core.api.Assertions.assertThat;
 
+import org.junit.jupiter.api.Test;
+
 class LagCalculationUtilsTest {
 
-    @Test
-    void getConsumerLag_normalCase() {
-        long lag = LagCalculationUtils.getConsumerLag(0, 50, 100);
-        assertThat(lag).isEqualTo(50);
-    }
+  @Test
+  void getConsumerLag_normalCase() {
+    long lag = LagCalculationUtils.getConsumerLag(0, 50, 100);
+    assertThat(lag).isEqualTo(50);
+  }
 
-    @Test
-    void getConsumerLag_noLag() {
-        long lag = LagCalculationUtils.getConsumerLag(0, 100, 100);
-        assertThat(lag).isEqualTo(0);
-    }
+  @Test
+  void getConsumerLag_noLag() {
+    long lag = LagCalculationUtils.getConsumerLag(0, 100, 100);
+    assertThat(lag).isEqualTo(0);
+  }
 
-    @Test
-    void getConsumerLag_neverCommitted() {
-        long lag = LagCalculationUtils.getConsumerLag(0, -1, 100);
-        assertThat(lag).isEqualTo(100);
-    }
+  @Test
+  void getConsumerLag_neverCommitted() {
+    long lag = LagCalculationUtils.getConsumerLag(0, -1, 100);
+    assertThat(lag).isEqualTo(100);
+  }
 
-    @Test
-    void getConsumerLag_committedBeforeLogStart() {
-        // When committed offset is before beginning (data deleted)
-        long lag = LagCalculationUtils.getConsumerLag(50, 10, 100);
-        assertThat(lag).isEqualTo(50); // end - beginning
-    }
+  @Test
+  void getConsumerLag_committedBeforeLogStart() {
+    // When committed offset is before beginning (data deleted)
+    long lag = LagCalculationUtils.getConsumerLag(50, 10, 100);
+    assertThat(lag).isEqualTo(50); // end - beginning
+  }
 
-    @Test
-    void getConsumerLag_emptyTopic() {
-        long lag = LagCalculationUtils.getConsumerLag(0, 0, 0);
-        assertThat(lag).isEqualTo(0);
-    }
+  @Test
+  void getConsumerLag_emptyTopic() {
+    long lag = LagCalculationUtils.getConsumerLag(0, 0, 0);
+    assertThat(lag).isEqualTo(0);
+  }
 
-    @Test
-    void getConsumerLagPercentage_halfWay() {
-        double percentage = LagCalculationUtils.getConsumerLagPercentage(0, 50, 100);
-        assertThat(percentage).isEqualTo(50.0);
-    }
+  @Test
+  void getConsumerLagPercentage_halfWay() {
+    double percentage = LagCalculationUtils.getConsumerLagPercentage(0, 50, 100);
+    assertThat(percentage).isEqualTo(50.0);
+  }
 
-    @Test
-    void getConsumerLagPercentage_noLag() {
-        double percentage = LagCalculationUtils.getConsumerLagPercentage(0, 100, 100);
-        assertThat(percentage).isEqualTo(0.0);
-    }
+  @Test
+  void getConsumerLagPercentage_noLag() {
+    double percentage = LagCalculationUtils.getConsumerLagPercentage(0, 100, 100);
+    assertThat(percentage).isEqualTo(0.0);
+  }
 
-    @Test
-    void getConsumerLagPercentage_fullLag() {
-        double percentage = LagCalculationUtils.getConsumerLagPercentage(0, 0, 100);
-        assertThat(percentage).isEqualTo(100.0);
-    }
+  @Test
+  void getConsumerLagPercentage_fullLag() {
+    double percentage = LagCalculationUtils.getConsumerLagPercentage(0, 0, 100);
+    assertThat(percentage).isEqualTo(100.0);
+  }
 
-    @Test
-    void getConsumerLagPercentage_emptyTopic() {
-        double percentage = LagCalculationUtils.getConsumerLagPercentage(0, 0, 0);
-        assertThat(percentage).isEqualTo(0.0);
-    }
+  @Test
+  void getConsumerLagPercentage_emptyTopic() {
+    double percentage = LagCalculationUtils.getConsumerLagPercentage(0, 0, 0);
+    assertThat(percentage).isEqualTo(0.0);
+  }
 
-    @Test
-    void getTimeSinceLastCommit_recentCommit() {
-        long commitTime = System.currentTimeMillis() - 1000;
-        long elapsed = LagCalculationUtils.getTimeSinceLastCommit(commitTime);
-        assertThat(elapsed).isBetween(1000L, 2000L);
-    }
+  @Test
+  void getTimeSinceLastCommit_recentCommit() {
+    long commitTime = System.currentTimeMillis() - 1000;
+    long elapsed = LagCalculationUtils.getTimeSinceLastCommit(commitTime);
+    assertThat(elapsed).isBetween(1000L, 2000L);
+  }
 
-    @Test
-    void getTimeSinceLastCommit_neverCommitted() {
-        long elapsed = LagCalculationUtils.getTimeSinceLastCommit(0);
-        assertThat(elapsed).isEqualTo(Long.MAX_VALUE);
-    }
+  @Test
+  void getTimeSinceLastCommit_neverCommitted() {
+    long elapsed = LagCalculationUtils.getTimeSinceLastCommit(0);
+    assertThat(elapsed).isEqualTo(Long.MAX_VALUE);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/processor/state/LastCommittedOffsetStateTest.java b/src/test/java/com/uber/ugroup/processor/state/LastCommittedOffsetStateTest.java
index 6e015b1..b6ebc6e 100644
--- a/src/test/java/com/uber/ugroup/processor/state/LastCommittedOffsetStateTest.java
+++ b/src/test/java/com/uber/ugroup/processor/state/LastCommittedOffsetStateTest.java
@@ -15,176 +15,175 @@
  */
 package com.uber.ugroup.processor.state;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import com.uber.ugroup.metrics.MetricsProvider;
 import com.uber.ugroup.model.GroupAndTopic;
 import com.uber.ugroup.model.LastCommittedOffset;
+import java.util.Map;
+import java.util.Set;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
-import java.util.Map;
-import java.util.Set;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
 @ExtendWith(MockitoExtension.class)
 class LastCommittedOffsetStateTest {
 
-    @Mock
-    private MetricsProvider metricsProvider;
-
-    private LastCommittedOffsetState state;
-
-    @BeforeEach
-    void setUp() {
-        state = new LastCommittedOffsetState(metricsProvider);
-    }
-
-    @Test
-    void updateAndGet() {
-        state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
-
-        GroupAndTopic gat = new GroupAndTopic("group1", "topic1");
-        Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
-
-        assertThat(offsets).hasSize(1);
-        assertThat(offsets.get(0).getOffset()).isEqualTo(100L);
-        assertThat(offsets.get(0).getCommitTimeMillis()).isEqualTo(1000L);
-    }
-
-    @Test
-    void updateMultiplePartitions() {
-        state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
-        state.updateLastCommittedOffset("group1", "topic1", 1, 200L, 2000L);
-        state.updateLastCommittedOffset("group1", "topic1", 2, 300L, 3000L);
+  @Mock private MetricsProvider metricsProvider;
 
-        GroupAndTopic gat = new GroupAndTopic("group1", "topic1");
-        Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
+  private LastCommittedOffsetState state;
 
-        assertThat(offsets).hasSize(3);
-        assertThat(offsets.get(0).getOffset()).isEqualTo(100L);
-        assertThat(offsets.get(1).getOffset()).isEqualTo(200L);
-        assertThat(offsets.get(2).getOffset()).isEqualTo(300L);
-    }
+  @BeforeEach
+  void setUp() {
+    state = new LastCommittedOffsetState(metricsProvider);
+  }
 
-    @Test
-    void update_overwritesExisting() {
-        state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
-        state.updateLastCommittedOffset("group1", "topic1", 0, 500L, 5000L);
+  @Test
+  void updateAndGet() {
+    state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
 
-        GroupAndTopic gat = new GroupAndTopic("group1", "topic1");
-        Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
+    GroupAndTopic gat = new GroupAndTopic("group1", "topic1");
+    Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
+
+    assertThat(offsets).hasSize(1);
+    assertThat(offsets.get(0).getOffset()).isEqualTo(100L);
+    assertThat(offsets.get(0).getCommitTimeMillis()).isEqualTo(1000L);
+  }
 
-        assertThat(offsets).hasSize(1);
-        assertThat(offsets.get(0).getOffset()).isEqualTo(500L);
-        assertThat(offsets.get(0).getCommitTimeMillis()).isEqualTo(5000L);
-    }
+  @Test
+  void updateMultiplePartitions() {
+    state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
+    state.updateLastCommittedOffset("group1", "topic1", 1, 200L, 2000L);
+    state.updateLastCommittedOffset("group1", "topic1", 2, 300L, 3000L);
 
-    @Test
-    void get_notPresent() {
-        GroupAndTopic gat = new GroupAndTopic("nonexistent", "topic");
-        Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
+    GroupAndTopic gat = new GroupAndTopic("group1", "topic1");
+    Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
 
-        assertThat(offsets).isEmpty();
-    }
+    assertThat(offsets).hasSize(3);
+    assertThat(offsets.get(0).getOffset()).isEqualTo(100L);
+    assertThat(offsets.get(1).getOffset()).isEqualTo(200L);
+    assertThat(offsets.get(2).getOffset()).isEqualTo(300L);
+  }
 
-    @Test
-    void get_returnsImmutableCopy() {
-        state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
+  @Test
+  void update_overwritesExisting() {
+    state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
+    state.updateLastCommittedOffset("group1", "topic1", 0, 500L, 5000L);
 
-        GroupAndTopic gat = new GroupAndTopic("group1", "topic1");
-        Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
+    GroupAndTopic gat = new GroupAndTopic("group1", "topic1");
+    Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
 
-        // The returned map should be an unmodifiable copy
-        assertThat(offsets).isNotNull();
-        assertThat(offsets.get(0).getOffset()).isEqualTo(100L);
-    }
+    assertThat(offsets).hasSize(1);
+    assertThat(offsets.get(0).getOffset()).isEqualTo(500L);
+    assertThat(offsets.get(0).getCommitTimeMillis()).isEqualTo(5000L);
+  }
 
-    @Test
-    void size_empty() {
-        assertThat(state.size()).isEqualTo(0);
-    }
+  @Test
+  void get_notPresent() {
+    GroupAndTopic gat = new GroupAndTopic("nonexistent", "topic");
+    Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
 
-    @Test
-    void size_afterUpdates() {
-        state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
-        state.updateLastCommittedOffset("group2", "topic2", 0, 200L, 2000L);
+    assertThat(offsets).isEmpty();
+  }
 
-        assertThat(state.size()).isEqualTo(2);
-    }
-
-    @Test
-    void size_sameGroupTopicDifferentPartitions() {
-        state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
-        state.updateLastCommittedOffset("group1", "topic1", 1, 200L, 2000L);
+  @Test
+  void get_returnsImmutableCopy() {
+    state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
 
-        // Same group-topic, different partitions should still count as 1 entry
-        assertThat(state.size()).isEqualTo(1);
-    }
+    GroupAndTopic gat = new GroupAndTopic("group1", "topic1");
+    Map offsets = state.getGroupTopicLastCommittedOffsets(gat);
 
-    @Test
-    void clear() {
-        state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
-        state.updateLastCommittedOffset("group2", "topic2", 0, 200L, 2000L);
+    // The returned map should be an unmodifiable copy
+    assertThat(offsets).isNotNull();
+    assertThat(offsets.get(0).getOffset()).isEqualTo(100L);
+  }
 
-        state.clear();
+  @Test
+  void size_empty() {
+    assertThat(state.size()).isEqualTo(0);
+  }
 
-        assertThat(state.size()).isEqualTo(0);
-        assertThat(state.getGroupTopicLastCommittedOffsets(new GroupAndTopic("group1", "topic1"))).isEmpty();
-    }
+  @Test
+  void size_afterUpdates() {
+    state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
+    state.updateLastCommittedOffset("group2", "topic2", 0, 200L, 2000L);
 
-    @Test
-    void invalidateUnassignedEntries() {
-        // Add entries for multiple groups
-        state.updateLastCommittedOffset("group-a", "topic1", 0, 100L, 1000L);
-        state.updateLastCommittedOffset("group-b", "topic1", 0, 200L, 2000L);
+    assertThat(state.size()).isEqualTo(2);
+  }
 
-        int totalPartitions = 50;
-        int partitionA = GroupAndTopic.calculatePartition("group-a", totalPartitions);
-        int partitionB = GroupAndTopic.calculatePartition("group-b", totalPartitions);
+  @Test
+  void size_sameGroupTopicDifferentPartitions() {
+    state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
+    state.updateLastCommittedOffset("group1", "topic1", 1, 200L, 2000L);
 
-        // Keep only partitionA assigned
-        Set assignedPartitions = Set.of(partitionA);
-        state.invalidateUnassignedEntries(assignedPartitions, totalPartitions);
-
-        GroupAndTopic gatA = new GroupAndTopic("group-a", "topic1");
-        GroupAndTopic gatB = new GroupAndTopic("group-b", "topic1");
-
-        // group-a should be retained since its partition is assigned
-        assertThat(state.getGroupTopicLastCommittedOffsets(gatA)).isNotEmpty();
-
-        // group-b should be removed if its partition differs from partitionA
-        if (partitionA != partitionB) {
-            assertThat(state.getGroupTopicLastCommittedOffsets(gatB)).isEmpty();
-        }
-    }
-
-    @Test
-    void invalidateUnassignedEntries_emptyAssigned() {
-        state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
-        state.updateLastCommittedOffset("group2", "topic2", 0, 200L, 2000L);
-
-        // No partitions assigned - everything should be removed
-        state.invalidateUnassignedEntries(Set.of(), 50);
-
-        assertThat(state.size()).isEqualTo(0);
-    }
+    // Same group-topic, different partitions should still count as 1 entry
+    assertThat(state.size()).isEqualTo(1);
+  }
 
-    @Test
-    void multipleGroupTopicCombinations() {
-        state.updateLastCommittedOffset("group1", "topicA", 0, 10L, 100L);
-        state.updateLastCommittedOffset("group1", "topicB", 0, 20L, 200L);
-        state.updateLastCommittedOffset("group2", "topicA", 0, 30L, 300L);
+  @Test
+  void clear() {
+    state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
+    state.updateLastCommittedOffset("group2", "topic2", 0, 200L, 2000L);
 
-        assertThat(state.size()).isEqualTo(3);
+    state.clear();
 
-        assertThat(state.getGroupTopicLastCommittedOffsets(new GroupAndTopic("group1", "topicA")))
-                .hasSize(1);
-        assertThat(state.getGroupTopicLastCommittedOffsets(new GroupAndTopic("group1", "topicB")))
-                .hasSize(1);
-        assertThat(state.getGroupTopicLastCommittedOffsets(new GroupAndTopic("group2", "topicA")))
-                .hasSize(1);
+    assertThat(state.size()).isEqualTo(0);
+    assertThat(state.getGroupTopicLastCommittedOffsets(new GroupAndTopic("group1", "topic1")))
+        .isEmpty();
+  }
+
+  @Test
+  void invalidateUnassignedEntries() {
+    // Add entries for multiple groups
+    state.updateLastCommittedOffset("group-a", "topic1", 0, 100L, 1000L);
+    state.updateLastCommittedOffset("group-b", "topic1", 0, 200L, 2000L);
+
+    int totalPartitions = 50;
+    int partitionA = GroupAndTopic.calculatePartition("group-a", totalPartitions);
+    int partitionB = GroupAndTopic.calculatePartition("group-b", totalPartitions);
+
+    // Keep only partitionA assigned
+    Set assignedPartitions = Set.of(partitionA);
+    state.invalidateUnassignedEntries(assignedPartitions, totalPartitions);
+
+    GroupAndTopic gatA = new GroupAndTopic("group-a", "topic1");
+    GroupAndTopic gatB = new GroupAndTopic("group-b", "topic1");
+
+    // group-a should be retained since its partition is assigned
+    assertThat(state.getGroupTopicLastCommittedOffsets(gatA)).isNotEmpty();
+
+    // group-b should be removed if its partition differs from partitionA
+    if (partitionA != partitionB) {
+      assertThat(state.getGroupTopicLastCommittedOffsets(gatB)).isEmpty();
     }
+  }
+
+  @Test
+  void invalidateUnassignedEntries_emptyAssigned() {
+    state.updateLastCommittedOffset("group1", "topic1", 0, 100L, 1000L);
+    state.updateLastCommittedOffset("group2", "topic2", 0, 200L, 2000L);
+
+    // No partitions assigned - everything should be removed
+    state.invalidateUnassignedEntries(Set.of(), 50);
+
+    assertThat(state.size()).isEqualTo(0);
+  }
+
+  @Test
+  void multipleGroupTopicCombinations() {
+    state.updateLastCommittedOffset("group1", "topicA", 0, 10L, 100L);
+    state.updateLastCommittedOffset("group1", "topicB", 0, 20L, 200L);
+    state.updateLastCommittedOffset("group2", "topicA", 0, 30L, 300L);
+
+    assertThat(state.size()).isEqualTo(3);
+
+    assertThat(state.getGroupTopicLastCommittedOffsets(new GroupAndTopic("group1", "topicA")))
+        .hasSize(1);
+    assertThat(state.getGroupTopicLastCommittedOffsets(new GroupAndTopic("group1", "topicB")))
+        .hasSize(1);
+    assertThat(state.getGroupTopicLastCommittedOffsets(new GroupAndTopic("group2", "topicA")))
+        .hasSize(1);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/watchlist/AllConsumersWatchListProviderTest.java b/src/test/java/com/uber/ugroup/watchlist/AllConsumersWatchListProviderTest.java
index 10e80bd..b09ec1a 100644
--- a/src/test/java/com/uber/ugroup/watchlist/AllConsumersWatchListProviderTest.java
+++ b/src/test/java/com/uber/ugroup/watchlist/AllConsumersWatchListProviderTest.java
@@ -15,88 +15,87 @@
  */
 package com.uber.ugroup.watchlist;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import com.uber.ugroup.model.ConsumerMetadata;
 import com.uber.ugroup.model.GroupAndTopic;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 class AllConsumersWatchListProviderTest {
 
-    private AllConsumersWatchListProvider provider;
-
-    @BeforeEach
-    void setUp() {
-        provider = new AllConsumersWatchListProvider();
-    }
-
-    @Test
-    void getName() {
-        assertThat(provider.getName()).isEqualTo("all");
-    }
-
-    @Test
-    void getPriority_isMaxValue() {
-        assertThat(provider.getPriority()).isEqualTo(Integer.MAX_VALUE);
-    }
-
-    @Test
-    void contains_alwaysReturnsTrue() {
-        assertThat(provider.contains(new GroupAndTopic("any-group", "any-topic"))).isTrue();
-    }
-
-    @Test
-    void contains_differentGroups_allReturnTrue() {
-        assertThat(provider.contains(new GroupAndTopic("group-a", "topic-1"))).isTrue();
-        assertThat(provider.contains(new GroupAndTopic("group-b", "topic-2"))).isTrue();
-        assertThat(provider.contains(new GroupAndTopic("", ""))).isTrue();
-    }
-
-    @Test
-    void getGroupAndTopics_returnsEmptyMap() {
-        Map> result = provider.getGroupAndTopics();
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    void getMetadata_returnsPresent() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        Optional metadata = provider.getMetadata(gat);
-        assertThat(metadata).isPresent();
-    }
-
-    @Test
-    void getMetadata_hasDefaultConsumerType() {
-        GroupAndTopic gat = new GroupAndTopic("group", "topic");
-        ConsumerMetadata metadata = provider.getMetadata(gat).get();
-        assertThat(metadata.getConsumerType()).isEqualTo("default");
-    }
-
-    @Test
-    void getMetadata_sameForAnyGroupAndTopic() {
-        ConsumerMetadata meta1 = provider.getMetadata(new GroupAndTopic("g1", "t1")).get();
-        ConsumerMetadata meta2 = provider.getMetadata(new GroupAndTopic("g2", "t2")).get();
-        assertThat(meta1).isSameAs(meta2);
-    }
-
-    @Test
-    void getGroupAndTopicsMetadata_returnsEmptyMap() {
-        Map> result = provider.getGroupAndTopicsMetadata();
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    void refresh_doesNotThrow() {
-        provider.refresh();
-    }
-
-    @Test
-    void isWatchListProvider() {
-        assertThat(provider).isInstanceOf(WatchListProvider.class);
-    }
+  private AllConsumersWatchListProvider provider;
+
+  @BeforeEach
+  void setUp() {
+    provider = new AllConsumersWatchListProvider();
+  }
+
+  @Test
+  void getName() {
+    assertThat(provider.getName()).isEqualTo("all");
+  }
+
+  @Test
+  void getPriority_isMaxValue() {
+    assertThat(provider.getPriority()).isEqualTo(Integer.MAX_VALUE);
+  }
+
+  @Test
+  void contains_alwaysReturnsTrue() {
+    assertThat(provider.contains(new GroupAndTopic("any-group", "any-topic"))).isTrue();
+  }
+
+  @Test
+  void contains_differentGroups_allReturnTrue() {
+    assertThat(provider.contains(new GroupAndTopic("group-a", "topic-1"))).isTrue();
+    assertThat(provider.contains(new GroupAndTopic("group-b", "topic-2"))).isTrue();
+    assertThat(provider.contains(new GroupAndTopic("", ""))).isTrue();
+  }
+
+  @Test
+  void getGroupAndTopics_returnsEmptyMap() {
+    Map> result = provider.getGroupAndTopics();
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void getMetadata_returnsPresent() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    Optional metadata = provider.getMetadata(gat);
+    assertThat(metadata).isPresent();
+  }
+
+  @Test
+  void getMetadata_hasDefaultConsumerType() {
+    GroupAndTopic gat = new GroupAndTopic("group", "topic");
+    ConsumerMetadata metadata = provider.getMetadata(gat).get();
+    assertThat(metadata.getConsumerType()).isEqualTo("default");
+  }
+
+  @Test
+  void getMetadata_sameForAnyGroupAndTopic() {
+    ConsumerMetadata meta1 = provider.getMetadata(new GroupAndTopic("g1", "t1")).get();
+    ConsumerMetadata meta2 = provider.getMetadata(new GroupAndTopic("g2", "t2")).get();
+    assertThat(meta1).isSameAs(meta2);
+  }
+
+  @Test
+  void getGroupAndTopicsMetadata_returnsEmptyMap() {
+    Map> result = provider.getGroupAndTopicsMetadata();
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  void refresh_doesNotThrow() {
+    provider.refresh();
+  }
+
+  @Test
+  void isWatchListProvider() {
+    assertThat(provider).isInstanceOf(WatchListProvider.class);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/watchlist/BlockListTest.java b/src/test/java/com/uber/ugroup/watchlist/BlockListTest.java
index f80ae41..43d74ba 100644
--- a/src/test/java/com/uber/ugroup/watchlist/BlockListTest.java
+++ b/src/test/java/com/uber/ugroup/watchlist/BlockListTest.java
@@ -15,193 +15,207 @@
  */
 package com.uber.ugroup.watchlist;
 
-import com.uber.ugroup.model.GroupAndTopic;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
+import static org.assertj.core.api.Assertions.assertThat;
 
+import com.uber.ugroup.model.GroupAndTopic;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 class BlockListTest {
 
-    @Test
-    void empty_returnsEmptyBlockList() {
-        BlockList blockList = BlockList.empty();
+  @Test
+  void empty_returnsEmptyBlockList() {
+    BlockList blockList = BlockList.empty();
 
-        assertThat(blockList.getBlockedGroups()).isEmpty();
-        assertThat(blockList.isBlocked(new GroupAndTopic("any-group", "any-topic"))).isFalse();
-        assertThat(blockList.isGroupBlocked("any-group")).isFalse();
-    }
+    assertThat(blockList.getBlockedGroups()).isEmpty();
+    assertThat(blockList.isBlocked(new GroupAndTopic("any-group", "any-topic"))).isFalse();
+    assertThat(blockList.isGroupBlocked("any-group")).isFalse();
+  }
 
-    @Test
-    void fromYaml_nullPath() {
-        BlockList blockList = BlockList.fromYaml(null);
+  @Test
+  void fromYaml_nullPath() {
+    BlockList blockList = BlockList.fromYaml(null);
 
-        assertThat(blockList.getBlockedGroups()).isEmpty();
-    }
+    assertThat(blockList.getBlockedGroups()).isEmpty();
+  }
 
-    @Test
-    void fromYaml_emptyPath() {
-        BlockList blockList = BlockList.fromYaml("");
+  @Test
+  void fromYaml_emptyPath() {
+    BlockList blockList = BlockList.fromYaml("");
 
-        assertThat(blockList.getBlockedGroups()).isEmpty();
-    }
+    assertThat(blockList.getBlockedGroups()).isEmpty();
+  }
 
-    @Test
-    void fromYaml_loadsGroups(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("blocklist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void fromYaml_loadsGroups(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("blocklist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 blocklist:
                   groups:
                     - blocked-group-1
                     - blocked-group-2
                 """);
 
-        BlockList blockList = BlockList.fromYaml(yamlFile.toString());
+    BlockList blockList = BlockList.fromYaml(yamlFile.toString());
 
-        assertThat(blockList.getBlockedGroups()).containsExactlyInAnyOrder("blocked-group-1", "blocked-group-2");
-    }
+    assertThat(blockList.getBlockedGroups())
+        .containsExactlyInAnyOrder("blocked-group-1", "blocked-group-2");
+  }
 
-    @Test
-    void fromYaml_loadsGroupPatterns(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("blocklist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void fromYaml_loadsGroupPatterns(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("blocklist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 blocklist:
                   group-patterns:
                     - "test-.*"
                     - ".*-staging"
                 """);
 
-        BlockList blockList = BlockList.fromYaml(yamlFile.toString());
+    BlockList blockList = BlockList.fromYaml(yamlFile.toString());
 
-        assertThat(blockList.isGroupBlocked("test-consumer")).isTrue();
-        assertThat(blockList.isGroupBlocked("my-app-staging")).isTrue();
-        assertThat(blockList.isGroupBlocked("production-consumer")).isFalse();
-    }
+    assertThat(blockList.isGroupBlocked("test-consumer")).isTrue();
+    assertThat(blockList.isGroupBlocked("my-app-staging")).isTrue();
+    assertThat(blockList.isGroupBlocked("production-consumer")).isFalse();
+  }
 
-    @Test
-    void fromYaml_loadsGroupTopics(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("blocklist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void fromYaml_loadsGroupTopics(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("blocklist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 blocklist:
                   group-topics:
                     - group: some-group
                       topic: some-topic
                 """);
 
-        BlockList blockList = BlockList.fromYaml(yamlFile.toString());
+    BlockList blockList = BlockList.fromYaml(yamlFile.toString());
 
-        assertThat(blockList.isBlocked(new GroupAndTopic("some-group", "some-topic"))).isTrue();
-        assertThat(blockList.isBlocked(new GroupAndTopic("some-group", "other-topic"))).isFalse();
-    }
+    assertThat(blockList.isBlocked(new GroupAndTopic("some-group", "some-topic"))).isTrue();
+    assertThat(blockList.isBlocked(new GroupAndTopic("some-group", "other-topic"))).isFalse();
+  }
 
-    @Test
-    void fromYaml_nonExistentFile() {
-        BlockList blockList = BlockList.fromYaml("/nonexistent/path/blocklist.yaml");
+  @Test
+  void fromYaml_nonExistentFile() {
+    BlockList blockList = BlockList.fromYaml("/nonexistent/path/blocklist.yaml");
 
-        assertThat(blockList.getBlockedGroups()).isEmpty();
-    }
+    assertThat(blockList.getBlockedGroups()).isEmpty();
+  }
 
-    @Test
-    void isBlocked_exactGroupMatch() {
-        BlockList blockList = BlockList.empty();
-        blockList.blockGroup("my-blocked-group");
+  @Test
+  void isBlocked_exactGroupMatch() {
+    BlockList blockList = BlockList.empty();
+    blockList.blockGroup("my-blocked-group");
 
-        assertThat(blockList.isBlocked(new GroupAndTopic("my-blocked-group", "any-topic"))).isTrue();
-        assertThat(blockList.isBlocked(new GroupAndTopic("other-group", "any-topic"))).isFalse();
-    }
+    assertThat(blockList.isBlocked(new GroupAndTopic("my-blocked-group", "any-topic"))).isTrue();
+    assertThat(blockList.isBlocked(new GroupAndTopic("other-group", "any-topic"))).isFalse();
+  }
 
-    @Test
-    void isBlocked_patternMatch(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("blocklist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void isBlocked_patternMatch(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("blocklist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 blocklist:
                   group-patterns:
                     - "test-.*"
                 """);
 
-        BlockList blockList = BlockList.fromYaml(yamlFile.toString());
+    BlockList blockList = BlockList.fromYaml(yamlFile.toString());
 
-        assertThat(blockList.isBlocked(new GroupAndTopic("test-consumer", "topic"))).isTrue();
-        assertThat(blockList.isBlocked(new GroupAndTopic("prod-consumer", "topic"))).isFalse();
-    }
+    assertThat(blockList.isBlocked(new GroupAndTopic("test-consumer", "topic"))).isTrue();
+    assertThat(blockList.isBlocked(new GroupAndTopic("prod-consumer", "topic"))).isFalse();
+  }
 
-    @Test
-    void isBlocked_groupTopicMatch(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("blocklist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void isBlocked_groupTopicMatch(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("blocklist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 blocklist:
                   group-topics:
                     - group: my-group
                       topic: blocked-topic
                 """);
 
-        BlockList blockList = BlockList.fromYaml(yamlFile.toString());
+    BlockList blockList = BlockList.fromYaml(yamlFile.toString());
 
-        assertThat(blockList.isBlocked(new GroupAndTopic("my-group", "blocked-topic"))).isTrue();
-        assertThat(blockList.isBlocked(new GroupAndTopic("my-group", "allowed-topic"))).isFalse();
-    }
+    assertThat(blockList.isBlocked(new GroupAndTopic("my-group", "blocked-topic"))).isTrue();
+    assertThat(blockList.isBlocked(new GroupAndTopic("my-group", "allowed-topic"))).isFalse();
+  }
 
-    @Test
-    void isGroupBlocked_exactMatch() {
-        BlockList blockList = BlockList.empty();
-        blockList.blockGroup("blocked");
+  @Test
+  void isGroupBlocked_exactMatch() {
+    BlockList blockList = BlockList.empty();
+    blockList.blockGroup("blocked");
 
-        assertThat(blockList.isGroupBlocked("blocked")).isTrue();
-        assertThat(blockList.isGroupBlocked("not-blocked")).isFalse();
-    }
+    assertThat(blockList.isGroupBlocked("blocked")).isTrue();
+    assertThat(blockList.isGroupBlocked("not-blocked")).isFalse();
+  }
 
-    @Test
-    void isGroupBlocked_patternMatch(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("blocklist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void isGroupBlocked_patternMatch(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("blocklist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 blocklist:
                   group-patterns:
                     - "internal-.*"
                 """);
 
-        BlockList blockList = BlockList.fromYaml(yamlFile.toString());
+    BlockList blockList = BlockList.fromYaml(yamlFile.toString());
 
-        assertThat(blockList.isGroupBlocked("internal-service")).isTrue();
-        assertThat(blockList.isGroupBlocked("external-service")).isFalse();
-    }
+    assertThat(blockList.isGroupBlocked("internal-service")).isTrue();
+    assertThat(blockList.isGroupBlocked("external-service")).isFalse();
+  }
 
-    @Test
-    void blockGroup_addsToBlockedGroups() {
-        BlockList blockList = BlockList.empty();
+  @Test
+  void blockGroup_addsToBlockedGroups() {
+    BlockList blockList = BlockList.empty();
 
-        blockList.blockGroup("new-blocked");
+    blockList.blockGroup("new-blocked");
 
-        assertThat(blockList.getBlockedGroups()).contains("new-blocked");
-        assertThat(blockList.isGroupBlocked("new-blocked")).isTrue();
-    }
+    assertThat(blockList.getBlockedGroups()).contains("new-blocked");
+    assertThat(blockList.isGroupBlocked("new-blocked")).isTrue();
+  }
 
-    @Test
-    void unblockGroup_removesFromBlockedGroups() {
-        BlockList blockList = BlockList.empty();
-        blockList.blockGroup("to-remove");
+  @Test
+  void unblockGroup_removesFromBlockedGroups() {
+    BlockList blockList = BlockList.empty();
+    blockList.blockGroup("to-remove");
 
-        blockList.unblockGroup("to-remove");
+    blockList.unblockGroup("to-remove");
 
-        assertThat(blockList.getBlockedGroups()).doesNotContain("to-remove");
-        assertThat(blockList.isGroupBlocked("to-remove")).isFalse();
-    }
+    assertThat(blockList.getBlockedGroups()).doesNotContain("to-remove");
+    assertThat(blockList.isGroupBlocked("to-remove")).isFalse();
+  }
 
-    @Test
-    void getBlockedGroups_returnsUnmodifiableSet() {
-        BlockList blockList = BlockList.empty();
-        blockList.blockGroup("group1");
+  @Test
+  void getBlockedGroups_returnsUnmodifiableSet() {
+    BlockList blockList = BlockList.empty();
+    blockList.blockGroup("group1");
 
-        assertThat(blockList.getBlockedGroups()).isUnmodifiable();
-    }
+    assertThat(blockList.getBlockedGroups()).isUnmodifiable();
+  }
 
-    @Test
-    void fromYaml_fullFile(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("blocklist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void fromYaml_fullFile(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("blocklist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 blocklist:
                   groups:
                     - exact-group
@@ -212,12 +226,12 @@ void fromYaml_fullFile(@TempDir Path tempDir) throws IOException {
                       topic: specific-topic
                 """);
 
-        BlockList blockList = BlockList.fromYaml(yamlFile.toString());
+    BlockList blockList = BlockList.fromYaml(yamlFile.toString());
 
-        assertThat(blockList.isBlocked(new GroupAndTopic("exact-group", "any-topic"))).isTrue();
-        assertThat(blockList.isBlocked(new GroupAndTopic("prefix-consumer", "any-topic"))).isTrue();
-        assertThat(blockList.isBlocked(new GroupAndTopic("specific-group", "specific-topic"))).isTrue();
-        assertThat(blockList.isBlocked(new GroupAndTopic("specific-group", "other-topic"))).isFalse();
-        assertThat(blockList.isBlocked(new GroupAndTopic("unrelated", "topic"))).isFalse();
-    }
+    assertThat(blockList.isBlocked(new GroupAndTopic("exact-group", "any-topic"))).isTrue();
+    assertThat(blockList.isBlocked(new GroupAndTopic("prefix-consumer", "any-topic"))).isTrue();
+    assertThat(blockList.isBlocked(new GroupAndTopic("specific-group", "specific-topic"))).isTrue();
+    assertThat(blockList.isBlocked(new GroupAndTopic("specific-group", "other-topic"))).isFalse();
+    assertThat(blockList.isBlocked(new GroupAndTopic("unrelated", "topic"))).isFalse();
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/watchlist/RegexWatchListProviderTest.java b/src/test/java/com/uber/ugroup/watchlist/RegexWatchListProviderTest.java
index a85ae4a..37691b7 100644
--- a/src/test/java/com/uber/ugroup/watchlist/RegexWatchListProviderTest.java
+++ b/src/test/java/com/uber/ugroup/watchlist/RegexWatchListProviderTest.java
@@ -15,62 +15,56 @@
  */
 package com.uber.ugroup.watchlist;
 
-import com.uber.ugroup.model.GroupAndTopic;
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
 
+import com.uber.ugroup.model.GroupAndTopic;
 import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
 
 class RegexWatchListProviderTest {
 
-    @Test
-    void contains_matchesIncludePattern() {
-        RegexWatchListProvider provider = new RegexWatchListProvider(List.of("production-.*"));
+  @Test
+  void contains_matchesIncludePattern() {
+    RegexWatchListProvider provider = new RegexWatchListProvider(List.of("production-.*"));
 
-        assertThat(provider.contains(new GroupAndTopic("production-consumer", "topic")))
-                .isTrue();
-        assertThat(provider.contains(new GroupAndTopic("staging-consumer", "topic")))
-                .isFalse();
-    }
+    assertThat(provider.contains(new GroupAndTopic("production-consumer", "topic"))).isTrue();
+    assertThat(provider.contains(new GroupAndTopic("staging-consumer", "topic"))).isFalse();
+  }
 
-    @Test
-    void contains_excludesPattern() {
-        RegexWatchListProvider provider = new RegexWatchListProvider(
-                List.of(".*"), // Include all
-                List.of("test-.*", ".*-dev") // Exclude test and dev
-        );
+  @Test
+  void contains_excludesPattern() {
+    RegexWatchListProvider provider =
+        new RegexWatchListProvider(
+            List.of(".*"), // Include all
+            List.of("test-.*", ".*-dev") // Exclude test and dev
+            );
 
-        assertThat(provider.contains(new GroupAndTopic("production-consumer", "topic")))
-                .isTrue();
-        assertThat(provider.contains(new GroupAndTopic("test-consumer", "topic")))
-                .isFalse();
-        assertThat(provider.contains(new GroupAndTopic("consumer-dev", "topic")))
-                .isFalse();
-    }
+    assertThat(provider.contains(new GroupAndTopic("production-consumer", "topic"))).isTrue();
+    assertThat(provider.contains(new GroupAndTopic("test-consumer", "topic"))).isFalse();
+    assertThat(provider.contains(new GroupAndTopic("consumer-dev", "topic"))).isFalse();
+  }
 
-    @Test
-    void contains_noIncludePatterns_acceptsAll() {
-        RegexWatchListProvider provider = new RegexWatchListProvider(
-                List.of(), // No includes = accept all
-                List.of("test-.*") // Except test
-        );
+  @Test
+  void contains_noIncludePatterns_acceptsAll() {
+    RegexWatchListProvider provider =
+        new RegexWatchListProvider(
+            List.of(), // No includes = accept all
+            List.of("test-.*") // Except test
+            );
 
-        assertThat(provider.contains(new GroupAndTopic("production-consumer", "topic")))
-                .isTrue();
-        assertThat(provider.contains(new GroupAndTopic("test-consumer", "topic")))
-                .isFalse();
-    }
+    assertThat(provider.contains(new GroupAndTopic("production-consumer", "topic"))).isTrue();
+    assertThat(provider.contains(new GroupAndTopic("test-consumer", "topic"))).isFalse();
+  }
 
-    @Test
-    void getName() {
-        RegexWatchListProvider provider = new RegexWatchListProvider(List.of(".*"));
-        assertThat(provider.getName()).isEqualTo("regex");
-    }
+  @Test
+  void getName() {
+    RegexWatchListProvider provider = new RegexWatchListProvider(List.of(".*"));
+    assertThat(provider.getName()).isEqualTo("regex");
+  }
 
-    @Test
-    void getPriority() {
-        RegexWatchListProvider provider = new RegexWatchListProvider(List.of(".*"));
-        assertThat(provider.getPriority()).isEqualTo(75);
-    }
+  @Test
+  void getPriority() {
+    RegexWatchListProvider provider = new RegexWatchListProvider(List.of(".*"));
+    assertThat(provider.getPriority()).isEqualTo(75);
+  }
 }
diff --git a/src/test/java/com/uber/ugroup/watchlist/StaticWatchListProviderTest.java b/src/test/java/com/uber/ugroup/watchlist/StaticWatchListProviderTest.java
index 6a3d360..879cdbe 100644
--- a/src/test/java/com/uber/ugroup/watchlist/StaticWatchListProviderTest.java
+++ b/src/test/java/com/uber/ugroup/watchlist/StaticWatchListProviderTest.java
@@ -15,83 +15,90 @@
  */
 package com.uber.ugroup.watchlist;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import com.uber.ugroup.model.ConsumerMetadata;
 import com.uber.ugroup.model.GroupAndTopic;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 class StaticWatchListProviderTest {
 
-    @Test
-    void getName() throws IOException {
-        StaticWatchListProvider provider = createProvider(minimalYaml());
-        assertThat(provider.getName()).isEqualTo("static");
-    }
-
-    @Test
-    void getPriority() throws IOException {
-        StaticWatchListProvider provider = createProvider(minimalYaml());
-        assertThat(provider.getPriority()).isEqualTo(50);
-    }
-
-    @Test
-    void contains_presentGroupAndTopic(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void getName() throws IOException {
+    StaticWatchListProvider provider = createProvider(minimalYaml());
+    assertThat(provider.getName()).isEqualTo("static");
+  }
+
+  @Test
+  void getPriority() throws IOException {
+    StaticWatchListProvider provider = createProvider(minimalYaml());
+    assertThat(provider.getPriority()).isEqualTo(50);
+  }
+
+  @Test
+  void contains_presentGroupAndTopic(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: my-group
                     topics:
                       - my-topic
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
 
-        assertThat(provider.contains(new GroupAndTopic("my-group", "my-topic"))).isTrue();
-    }
+    assertThat(provider.contains(new GroupAndTopic("my-group", "my-topic"))).isTrue();
+  }
 
-    @Test
-    void contains_absentGroupAndTopic(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void contains_absentGroupAndTopic(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: my-group
                     topics:
                       - my-topic
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
 
-        assertThat(provider.contains(new GroupAndTopic("other-group", "other-topic"))).isFalse();
-    }
+    assertThat(provider.contains(new GroupAndTopic("other-group", "other-topic"))).isFalse();
+  }
 
-    @Test
-    void contains_sameGroupDifferentTopic(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void contains_sameGroupDifferentTopic(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: my-group
                     topics:
                       - topic-a
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
 
-        assertThat(provider.contains(new GroupAndTopic("my-group", "topic-b"))).isFalse();
-    }
+    assertThat(provider.contains(new GroupAndTopic("my-group", "topic-b"))).isFalse();
+  }
 
-    @Test
-    void getGroupAndTopics_partitionMapping(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void getGroupAndTopics_partitionMapping(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: group-a
                     topics:
@@ -102,24 +109,26 @@ void getGroupAndTopics_partitionMapping(@TempDir Path tempDir) throws IOExceptio
                       - topic-3
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString(), 50);
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString(), 50);
 
-        Map> byPartition = provider.getGroupAndTopics();
+    Map> byPartition = provider.getGroupAndTopics();
 
-        // All 3 group-topic entries should be present across partitions
-        long totalEntries = byPartition.values().stream().mapToLong(Set::size).sum();
-        assertThat(totalEntries).isEqualTo(3);
+    // All 3 group-topic entries should be present across partitions
+    long totalEntries = byPartition.values().stream().mapToLong(Set::size).sum();
+    assertThat(totalEntries).isEqualTo(3);
 
-        // Verify each entry lands in the correct partition
-        GroupAndTopic gatA1 = new GroupAndTopic("group-a", "topic-1").withPartitionCount(50);
-        int expectedPartition = gatA1.getConsumerOffsetsPartition();
-        assertThat(byPartition.get(expectedPartition)).contains(gatA1);
-    }
+    // Verify each entry lands in the correct partition
+    GroupAndTopic gatA1 = new GroupAndTopic("group-a", "topic-1").withPartitionCount(50);
+    int expectedPartition = gatA1.getConsumerOffsetsPartition();
+    assertThat(byPartition.get(expectedPartition)).contains(gatA1);
+  }
 
-    @Test
-    void getGroupAndTopics_multipleTopicsSameGroup(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void getGroupAndTopics_multipleTopicsSameGroup(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: same-group
                     topics:
@@ -127,22 +136,24 @@ void getGroupAndTopics_multipleTopicsSameGroup(@TempDir Path tempDir) throws IOE
                       - topic-y
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString(), 50);
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString(), 50);
 
-        // Both topics under same group should share the same partition
-        GroupAndTopic gatX = new GroupAndTopic("same-group", "topic-x").withPartitionCount(50);
-        GroupAndTopic gatY = new GroupAndTopic("same-group", "topic-y").withPartitionCount(50);
+    // Both topics under same group should share the same partition
+    GroupAndTopic gatX = new GroupAndTopic("same-group", "topic-x").withPartitionCount(50);
+    GroupAndTopic gatY = new GroupAndTopic("same-group", "topic-y").withPartitionCount(50);
 
-        assertThat(gatX.getConsumerOffsetsPartition()).isEqualTo(gatY.getConsumerOffsetsPartition());
+    assertThat(gatX.getConsumerOffsetsPartition()).isEqualTo(gatY.getConsumerOffsetsPartition());
 
-        int partition = gatX.getConsumerOffsetsPartition();
-        assertThat(provider.getGroupAndTopics().get(partition)).contains(gatX, gatY);
-    }
+    int partition = gatX.getConsumerOffsetsPartition();
+    assertThat(provider.getGroupAndTopics().get(partition)).contains(gatX, gatY);
+  }
 
-    @Test
-    void getMetadata_withMetadata(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void getMetadata_withMetadata(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: my-group
                     topics:
@@ -151,50 +162,58 @@ void getMetadata_withMetadata(@TempDir Path tempDir) throws IOException {
                       type: batch
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
 
-        Optional meta = provider.getMetadata(new GroupAndTopic("my-group", "my-topic"));
-        assertThat(meta).isPresent();
-        assertThat(meta.get().getConsumerType()).isEqualTo("batch");
-    }
+    Optional meta =
+        provider.getMetadata(new GroupAndTopic("my-group", "my-topic"));
+    assertThat(meta).isPresent();
+    assertThat(meta.get().getConsumerType()).isEqualTo("batch");
+  }
 
-    @Test
-    void getMetadata_withoutMetadata(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void getMetadata_withoutMetadata(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: my-group
                     topics:
                       - my-topic
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
 
-        Optional meta = provider.getMetadata(new GroupAndTopic("my-group", "my-topic"));
-        assertThat(meta).isPresent();
-        assertThat(meta.get().getConsumerType()).isEqualTo("static");
-    }
+    Optional meta =
+        provider.getMetadata(new GroupAndTopic("my-group", "my-topic"));
+    assertThat(meta).isPresent();
+    assertThat(meta.get().getConsumerType()).isEqualTo("static");
+  }
 
-    @Test
-    void getMetadata_unknownGroupAndTopic(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void getMetadata_unknownGroupAndTopic(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: my-group
                     topics:
                       - my-topic
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
 
-        Optional meta = provider.getMetadata(new GroupAndTopic("unknown", "unknown"));
-        assertThat(meta).isEmpty();
-    }
+    Optional meta = provider.getMetadata(new GroupAndTopic("unknown", "unknown"));
+    assertThat(meta).isEmpty();
+  }
 
-    @Test
-    void getGroupAndTopicsMetadata(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void getGroupAndTopicsMetadata(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: group-a
                     topics:
@@ -206,91 +225,96 @@ void getGroupAndTopicsMetadata(@TempDir Path tempDir) throws IOException {
                       - topic-2
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
-
-        Map> allMetadata = provider.getGroupAndTopicsMetadata();
-        assertThat(allMetadata).containsKeys("group-a", "group-b");
-        assertThat(allMetadata.get("group-a")).containsKey("topic-1");
-        assertThat(allMetadata.get("group-a").get("topic-1").getConsumerType()).isEqualTo("streaming");
-        assertThat(allMetadata.get("group-b").get("topic-2").getConsumerType()).isEqualTo("static");
-    }
-
-    @Test
-    void refresh_reloadsFromFile(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
+
+    Map> allMetadata = provider.getGroupAndTopicsMetadata();
+    assertThat(allMetadata).containsKeys("group-a", "group-b");
+    assertThat(allMetadata.get("group-a")).containsKey("topic-1");
+    assertThat(allMetadata.get("group-a").get("topic-1").getConsumerType()).isEqualTo("streaming");
+    assertThat(allMetadata.get("group-b").get("topic-2").getConsumerType()).isEqualTo("static");
+  }
+
+  @Test
+  void refresh_reloadsFromFile(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: initial-group
                     topics:
                       - initial-topic
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
-        assertThat(provider.contains(new GroupAndTopic("initial-group", "initial-topic"))).isTrue();
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString());
+    assertThat(provider.contains(new GroupAndTopic("initial-group", "initial-topic"))).isTrue();
 
-        // Update the file
-        Files.writeString(yamlFile, """
+    // Update the file
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: updated-group
                     topics:
                       - updated-topic
                 """);
 
-        provider.refresh();
+    provider.refresh();
 
-        assertThat(provider.contains(new GroupAndTopic("updated-group", "updated-topic"))).isTrue();
-        assertThat(provider.contains(new GroupAndTopic("initial-group", "initial-topic"))).isFalse();
-    }
+    assertThat(provider.contains(new GroupAndTopic("updated-group", "updated-topic"))).isTrue();
+    assertThat(provider.contains(new GroupAndTopic("initial-group", "initial-topic"))).isFalse();
+  }
 
-    @Test
-    void constructor_nullPath() {
-        StaticWatchListProvider provider = new StaticWatchListProvider(null);
+  @Test
+  void constructor_nullPath() {
+    StaticWatchListProvider provider = new StaticWatchListProvider(null);
 
-        assertThat(provider.getGroupAndTopics()).isEmpty();
-        assertThat(provider.contains(new GroupAndTopic("any", "any"))).isFalse();
-    }
+    assertThat(provider.getGroupAndTopics()).isEmpty();
+    assertThat(provider.contains(new GroupAndTopic("any", "any"))).isFalse();
+  }
 
-    @Test
-    void constructor_emptyPath() {
-        StaticWatchListProvider provider = new StaticWatchListProvider("");
+  @Test
+  void constructor_emptyPath() {
+    StaticWatchListProvider provider = new StaticWatchListProvider("");
 
-        assertThat(provider.getGroupAndTopics()).isEmpty();
-    }
+    assertThat(provider.getGroupAndTopics()).isEmpty();
+  }
 
-    @Test
-    void constructor_customPartitionCount(@TempDir Path tempDir) throws IOException {
-        Path yamlFile = tempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, """
+  @Test
+  void constructor_customPartitionCount(@TempDir Path tempDir) throws IOException {
+    Path yamlFile = tempDir.resolve("watchlist.yaml");
+    Files.writeString(
+        yamlFile,
+        """
                 watchlist:
                   - group: my-group
                     topics:
                       - my-topic
                 """);
 
-        StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString(), 10);
+    StaticWatchListProvider provider = new StaticWatchListProvider(yamlFile.toString(), 10);
 
-        GroupAndTopic expected = new GroupAndTopic("my-group", "my-topic").withPartitionCount(10);
-        int partition = expected.getConsumerOffsetsPartition();
-        assertThat(provider.getGroupAndTopics().get(partition)).contains(expected);
-    }
+    GroupAndTopic expected = new GroupAndTopic("my-group", "my-topic").withPartitionCount(10);
+    int partition = expected.getConsumerOffsetsPartition();
+    assertThat(provider.getGroupAndTopics().get(partition)).contains(expected);
+  }
 
-    // --- Helpers ---
+  // --- Helpers ---
 
-    @TempDir
-    Path sharedTempDir;
+  @TempDir Path sharedTempDir;
 
-    private StaticWatchListProvider createProvider(String yamlContent) throws IOException {
-        Path yamlFile = sharedTempDir.resolve("watchlist.yaml");
-        Files.writeString(yamlFile, yamlContent);
-        return new StaticWatchListProvider(yamlFile.toString());
-    }
+  private StaticWatchListProvider createProvider(String yamlContent) throws IOException {
+    Path yamlFile = sharedTempDir.resolve("watchlist.yaml");
+    Files.writeString(yamlFile, yamlContent);
+    return new StaticWatchListProvider(yamlFile.toString());
+  }
 
-    private String minimalYaml() {
-        return """
+  private String minimalYaml() {
+    return """
                 watchlist:
                   - group: test-group
                     topics:
                       - test-topic
                 """;
-    }
+  }
 }