← Back to Articles
6/8/2026Guest Transmission

sliding window comprehensive

Sliding Window: The Complete Professional Guide

From Zero to Expert - Everything you need to master the Sliding Window pattern across algorithms, system design, real-world engineering, and interview preparation.


Table of Contents

  1. The Mental Model - What Is a Sliding Window?
  2. Why Sliding Window Exists - The Problem It Solves
  3. Anatomy of a Sliding Window
  4. Types of Sliding Windows
  5. How to Identify a Sliding Window Problem
  6. The Universal Template
  7. Fixed-Size Window Problems - Deep Dive
  8. Variable-Size Window Problems - Deep Dive
  9. Advanced Sliding Window Techniques
  10. Real-World Applications Beyond Algorithms
  11. Circuit Breaker Pattern and Sliding Windows
  12. Networking and Protocol Engineering
  13. Databases, Analytics and Streaming Systems
  14. Machine Learning and Signal Processing
  15. Common Pitfalls and How to Avoid Them
  16. Interview Preparation - Strategy and Patterns
  17. Problem Catalog by Difficulty
  18. Full Implementations in Java and Python
  19. Complexity Cheat Sheet
  20. Decision Tree - Which Window Type to Use

1. The Mental Model

The Everyday Analogy

Imagine you are sitting on a train looking out of the window. You can only see a certain stretch of landscape at any given moment. As the train moves forward, new scenery enters your view from the right, and old scenery disappears from the left. You are always observing a fixed or adjustable portion of the total landscape.

This is precisely what a sliding window does in computer science:

  • The train window is your algorithm's window of observation.
  • The landscape is your data array, string, or stream.
  • The train moving forward is your iteration through the data.
  • What you observe from the window is what your algorithm processes at each step.

The Core Insight

Most brute-force solutions over arrays or strings involve picking every possible subarray or substring and checking it independently. This leads to redundant computation because adjacent subarrays overlap significantly.

Consider finding the maximum sum of any 3 consecutive elements in:

[2, 1, 5, 1, 3, 2]

Brute force evaluates:

  • [2, 1, 5] = 8
  • [1, 5, 1] = 7
  • [5, 1, 3] = 9
  • [1, 3, 2] = 6

Notice that [2, 1, 5] and [1, 5, 1] share two elements [1, 5]. You are computing 1 + 5 twice. With a window of size 1000 in an array of size 1 million, you recompute 999 elements every single step. That is catastrophic waste.

Sliding window says: when you move the window one step right, subtract the element leaving on the left and add the element entering on the right. You reuse all the prior computation.

This single insight transforms O(n * k) brute force into O(n) sliding window. This is the entire foundation.


2. Why Sliding Window Exists - The Problem It Solves

The Complexity Crisis in Substring and Subarray Problems

Without sliding window, the naive approach to most subarray/substring problems follows this pattern:

for every starting index i:
    for every ending index j >= i:
        examine the subarray from i to j

This is O(n^2) or O(n^3) depending on how much work is done inside. For modern applications dealing with gigabytes of streaming data, log files, network packets, or financial tick data, quadratic complexity is completely unacceptable.

The Mathematical Justification

Suppose you have an array of size n and you want to examine every subarray of size k.

  • There are n - k + 1 such subarrays.
  • Each subarray has k elements.
  • Brute force cost: (n - k + 1) * k which is approximately O(n * k).

With sliding window:

  • You process each element at most twice (once when it enters, once when it exits).
  • Total cost: O(n) regardless of k.

For n = 10^6 and k = 10^3, that is the difference between 10^9 operations and 10^6 operations - a 1000x speedup.


3. Anatomy of a Sliding Window

Every sliding window solution, regardless of complexity, has the following components:

3.1 The Two Pointers

left  ---->                    right ---->
  |                              |
  v                              v
[ a,  b,  c,  d,  e,  f,  g,  h,  i ]
  |__________________________|
           Window
  • left pointer (also called start or i): marks the beginning of the current window. It moves forward to shrink the window.
  • right pointer (also called end or j): marks the end of the current window. It moves forward to expand the window.

3.2 The Window State

The window state is a data structure that tracks the aggregate or summary of what is inside the current window. Depending on the problem, this could be:

Problem TypeWindow State Data Structure
Sum of elementsA single integer variable
Character frequencyA HashMap or array of size 26
Distinct elementsA HashSet
Ordered elementsA monotonic deque
Count of condition violationsAn integer counter
Product of elementsA single long variable

3.3 The Expansion Step

When the right pointer moves forward, the new element arr[right] is incorporated into the window state. This is an O(1) or O(log n) operation depending on the state structure.

3.4 The Contraction Step

When the window violates a constraint (in variable windows) or reaches maximum size (in fixed windows), the left pointer moves forward, removing arr[left] from the window state.

3.5 The Answer Update

At certain points during iteration - either when the window is at full size (fixed), or when the window satisfies the constraint (variable) - the answer is updated.


4. Types of Sliding Windows

4.1 Fixed-Size Window

The window size k is given upfront and never changes. The right pointer moves forward, and whenever the window exceeds size k, the left pointer also moves forward in lockstep, maintaining exactly k elements.

Window size = 3

Step 1:  [a  b  c] d  e  f
Step 2:   a [b  c  d] e  f
Step 3:   a  b [c  d  e] f
Step 4:   a  b  c [d  e  f]

Use case trigger words: "exactly k", "window of size k", "every k consecutive", "last k elements".

4.2 Variable-Size Window (Grow and Shrink)

The window size is not fixed. The right pointer moves to expand the window as long as a constraint is satisfied. When the constraint is violated, the left pointer moves forward to restore validity. The answer is updated whenever the window is valid.

Find longest subarray with sum <= 10
Array: [1, 2, 3, 4, 5]

right=0: [1]         sum=1  valid
right=1: [1,2]       sum=3  valid
right=2: [1,2,3]     sum=6  valid
right=3: [1,2,3,4]   sum=10 valid (answer=4)
right=4: [1,2,3,4,5] sum=15 INVALID -> shrink
         left moves: [2,3,4,5] sum=14 still invalid
         left moves: [3,4,5]   sum=12 still invalid
         left moves: [4,5]     sum=9  valid (answer still 4)

4.3 Variable-Size Window (Find Minimum)

Similar to 4.2 but with the opposite direction: shrink the window aggressively as long as the constraint continues to be satisfied, to find the minimum valid window.

Find shortest subarray with sum >= 7
Array: [2, 3, 1, 2, 4, 3]

Expand until sum >= 7, then shrink as much as possible while maintaining sum >= 7.

4.4 Multi-Window (Two Simultaneous Windows)

Some problems require maintaining two windows - for example, comparing a fixed-size window from the front and back of an array simultaneously, or maintaining a window in two correlated arrays.

4.5 Sliding Window with Auxiliary Data Structure

The most complex variant. The window itself is variable or fixed, but maintaining the window state requires an auxiliary structure like a monotonic deque, a sorted set, or a segment tree.

Example: Maximum in every window of size k - requires a monotonic deque to answer each query in O(1) amortized.


5. How to Identify a Sliding Window Problem

This is the most critical skill in interviews and competitive programming. Here is a systematic framework to determine if a problem needs a sliding window.

5.1 The Five Signals

Signal 1 - The Data is Sequential and Contiguous

The problem operates on a contiguous portion of an array, string, or stream. Key phrases:

  • "subarray" or "substring" (not subsequence - subsequence can skip elements)
  • "contiguous elements"
  • "consecutive"
  • "window"

Note: If the problem says "subsequence" (non-contiguous), sliding window likely does not apply directly.

Signal 2 - An Optimization or Constraint on the Window

The problem asks for the best (maximum, minimum, longest, shortest) contiguous section that satisfies some constraint. Examples:

  • Longest substring with at most k distinct characters
  • Smallest subarray with sum >= target
  • Maximum average subarray of length k
  • Minimum window substring containing all characters of a pattern

Signal 3 - The Constraint is Monotonic

This is the deeper mathematical reason sliding window works. When you expand the window (add an element on the right), the constraint value either monotonically increases or decreases. When you shrink the window (remove from left), it monotonically moves in the other direction.

Example: In "longest subarray with sum <= target", the sum only increases when you add elements and only decreases when you remove them. This monotonicity is what makes it safe to move pointers in one direction only, never backtracking.

If the constraint is not monotonic (e.g., sum could go up and down because of negative numbers), pure sliding window may not work.

Signal 4 - Brute Force is O(n^2) and You Need Better

A strong interview hint: if you find yourself writing nested loops over adjacent indices, stop and ask yourself if sliding window can eliminate one loop.

Signal 5 - The Problem Involves "At Most K" or "Exactly K"

These are classic sliding window triggers:

  • "at most k distinct characters" - variable window
  • "exactly k occurrences" - often solved as (at most k) minus (at most k-1)
  • "k consecutive elements" - fixed window

5.2 The Contrast: What is NOT a Sliding Window Problem

ScenarioWhy Not Sliding Window
Find all subsequences (non-contiguous)Elements can be skipped; no contiguity
Problems with negative numbers in sum constraintsSum is not monotonic; window can grow and shrink unpredictably
2D matrix window problemsRequire different techniques (prefix sums)
Finding global min/max without a window constraintSimply iterate; no window needed

5.3 Decision Questions to Ask Yourself

1. Is the input a linear structure (array, string, stream)?
   NO  -> Sliding window probably does not apply
   YES -> Continue

2. Does the problem involve a contiguous portion of the input?
   NO  -> Probably not sliding window
   YES -> Continue

3. Is there a constraint or optimization on that portion?
   NO  -> Simple iteration might suffice
   YES -> Continue

4. Does the constraint behave monotonically as the window grows or shrinks?
   NO  -> Consider other approaches (prefix sum, DP)
   YES -> Strong candidate for sliding window

5. Is the window size fixed or variable?
   FIXED    -> Use fixed-size template
   VARIABLE -> Use expand-and-shrink template

6. The Universal Template

These templates handle the vast majority of sliding window problems. Understand them deeply - they are the backbone.

6.1 Fixed-Size Window Template (Java)

public int fixedWindowTemplate(int[] arr, int k) {
    int n = arr.length;
    if (n < k) return -1; // edge case: array smaller than window
 
    // Step 1: Initialize the first window
    int windowState = 0;
    for (int i = 0; i < k; i++) {
        windowState += arr[i]; // or whatever operation defines the state
    }
 
    int answer = windowState;
 
    // Step 2: Slide the window from position k to n-1
    for (int right = k; right < n; right++) {
        // Expand: add the incoming element on the right
        windowState += arr[right];
 
        // Contract: remove the outgoing element on the left
        // The outgoing element is at index (right - k)
        windowState -= arr[right - k];
 
        // Update answer
        answer = Math.max(answer, windowState);
    }
 
    return answer;
}

6.2 Fixed-Size Window Template (Python)

def fixed_window_template(arr: list, k: int) -> int:
    n = len(arr)
    if n < k:
        return -1  # edge case
 
    # Initialize the first window
    window_state = sum(arr[:k])
    answer = window_state
 
    # Slide the window
    for right in range(k, n):
        # Expand: add incoming element
        window_state += arr[right]
 
        # Contract: remove outgoing element
        # The outgoing element is at index (right - k)
        window_state -= arr[right - k]
 
        # Update answer
        answer = max(answer, window_state)
 
    return answer

6.3 Variable-Size Window - Maximize Length Template (Java)

public int variableWindowMaxLength(int[] arr, int constraint) {
    int left = 0;
    int windowState = 0; // represents the current window's metric
    int answer = 0;
 
    for (int right = 0; right < arr.length; right++) {
        // Expand: incorporate arr[right] into the window state
        windowState = incorporate(windowState, arr[right]);
 
        // Contract: while the window is invalid, shrink from the left
        while (isInvalid(windowState, constraint)) {
            windowState = remove(windowState, arr[left]);
            left++;
        }
 
        // At this point, window [left, right] is valid
        // Update the answer with the current valid window size
        answer = Math.max(answer, right - left + 1);
    }
 
    return answer;
}

6.4 Variable-Size Window - Minimize Length Template (Java)

public int variableWindowMinLength(int[] arr, int target) {
    int left = 0;
    int windowState = 0;
    int answer = Integer.MAX_VALUE;
 
    for (int right = 0; right < arr.length; right++) {
        // Expand
        windowState = incorporate(windowState, arr[right]);
 
        // Shrink aggressively as long as the window remains valid
        while (isValid(windowState, target)) {
            // Record the current valid window (try to minimize it)
            answer = Math.min(answer, right - left + 1);
 
            // Now shrink to see if we can find an even smaller valid window
            windowState = remove(windowState, arr[left]);
            left++;
        }
    }
 
    return answer == Integer.MAX_VALUE ? 0 : answer;
}

6.5 Variable-Size Window Templates (Python)

def variable_window_max_length(arr: list, constraint: int) -> int:
    left = 0
    window_state = {}  # or 0, or set() - depends on problem
    answer = 0
 
    for right in range(len(arr)):
        # Expand: add arr[right]
        window_state[arr[right]] = window_state.get(arr[right], 0) + 1
 
        # Contract: while invalid
        while not is_valid(window_state, constraint):
            window_state[arr[left]] -= 1
            if window_state[arr[left]] == 0:
                del window_state[arr[left]]
            left += 1
 
        # Window [left, right] is now valid
        answer = max(answer, right - left + 1)
 
    return answer
 
 
def variable_window_min_length(arr: list, target: int) -> int:
    left = 0
    current_sum = 0
    answer = float('inf')
 
    for right in range(len(arr)):
        current_sum += arr[right]
 
        while current_sum >= target:
            answer = min(answer, right - left + 1)
            current_sum -= arr[left]
            left += 1
 
    return 0 if answer == float('inf') else answer

7. Fixed-Size Window Problems - Deep Dive

7.1 Maximum Sum Subarray of Size K

Problem: Given an array of integers and a number k, find the maximum sum of any contiguous subarray of size k.

Why sliding window: Window size is exactly k (fixed). Sum increases/decreases monotonically with additions/removals.

Java Implementation:

public class MaxSumSubarrayK {
 
    public static int maxSum(int[] arr, int k) {
        int n = arr.length;
        if (n < k) {
            throw new IllegalArgumentException("Array size smaller than k");
        }
 
        // Build the first window
        int currentSum = 0;
        for (int i = 0; i < k; i++) {
            currentSum += arr[i];
        }
 
        int maxSum = currentSum;
 
        // Slide the window across the rest of the array
        for (int right = k; right < n; right++) {
            // The magic: O(1) update instead of recomputing the entire sum
            currentSum += arr[right];       // add new element entering the window
            currentSum -= arr[right - k];   // remove element leaving the window
 
            maxSum = Math.max(maxSum, currentSum);
        }
 
        return maxSum;
    }
 
    public static void main(String[] args) {
        int[] arr = {2, 1, 5, 1, 3, 2};
        int k = 3;
        System.out.println(maxSum(arr, k)); // Output: 9 (subarray [5,1,3])
    }
}

Python Implementation:

def max_sum_subarray_k(arr: list[int], k: int) -> int:
    if len(arr) < k:
        raise ValueError("Array size smaller than k")
 
    # Build the first window
    current_sum = sum(arr[:k])
    max_sum = current_sum
 
    # Slide
    for right in range(k, len(arr)):
        current_sum += arr[right]
        current_sum -= arr[right - k]
        max_sum = max(max_sum, current_sum)
 
    return max_sum
 
# Test
arr = [2, 1, 5, 1, 3, 2]
print(max_sum_subarray_k(arr, 3))  # Output: 9

Complexity: Time O(n), Space O(1)


7.2 Maximum Average Subarray of Size K

Problem: Find the contiguous subarray of length exactly k that has the maximum average value.

Insight: Maximizing average is equivalent to maximizing sum for fixed-size windows.

Java Implementation:

public double findMaxAverage(int[] nums, int k) {
    double windowSum = 0;
    for (int i = 0; i < k; i++) {
        windowSum += nums[i];
    }
 
    double maxSum = windowSum;
 
    for (int right = k; right < nums.length; right++) {
        windowSum += nums[right];
        windowSum -= nums[right - k];
        maxSum = Math.max(maxSum, windowSum);
    }
 
    return maxSum / k;
}

7.3 Find All Anagrams in a String (LeetCode 438)

Problem: Given string s and pattern p, find all starting indices of anagram occurrences of p in s.

Why sliding window: Window size is fixed at p.length(). State is a frequency map. Two frequency maps are equal when they have the same character counts.

Key insight: Instead of comparing two hash maps at every step (expensive), maintain a matches counter that tracks how many characters currently have matching frequencies.

Java Implementation:

import java.util.*;
 
public class FindAllAnagrams {
 
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList<>();
        if (s.length() < p.length()) return result;
 
        int[] pCount = new int[26];
        int[] sCount = new int[26];
        int k = p.length();
 
        // Build frequency maps for the pattern and the first window
        for (int i = 0; i < k; i++) {
            pCount[p.charAt(i) - 'a']++;
            sCount[s.charAt(i) - 'a']++;
        }
 
        // Count how many characters already match
        int matches = 0;
        for (int i = 0; i < 26; i++) {
            if (pCount[i] <mark class="obsidian-highlight"> sCount[i]) matches++;
        }
 
        if (matches </mark> 26) result.add(0);
 
        for (int right = k; right < s.length(); right++) {
            // Add the new character entering the window
            int incoming = s.charAt(right) - 'a';
            sCount[incoming]++;
            if (sCount[incoming] <mark class="obsidian-highlight"> pCount[incoming]) {
                matches++;
            } else if (sCount[incoming] </mark> pCount[incoming] + 1) {
                // This character just became mismatched (was previously matched)
                matches--;
            }
 
            // Remove the character leaving the window
            int outgoing = s.charAt(right - k) - 'a';
            sCount[outgoing]--;
            if (sCount[outgoing] <mark class="obsidian-highlight"> pCount[outgoing]) {
                matches++;
            } else if (sCount[outgoing] </mark> pCount[outgoing] - 1) {
                matches--;
            }
 
            if (matches == 26) result.add(right - k + 1);
        }
 
        return result;
    }
}

Python Implementation:

from collections import Counter
 
def find_anagrams(s: str, p: str) -> list[int]:
    if len(s) < len(p):
        return []
 
    k = len(p)
    p_count = Counter(p)
    s_count = Counter(s[:k])
    result = []
 
    if s_count == p_count:
        result.append(0)
 
    for right in range(k, len(s)):
        # Add incoming character
        incoming = s[right]
        s_count[incoming] += 1
 
        # Remove outgoing character
        outgoing = s[right - k]
        s_count[outgoing] -= 1
        if s_count[outgoing] <mark class="obsidian-highlight"> 0:
            del s_count[outgoing]
 
        if s_count </mark> p_count:
            result.append(right - k + 1)
 
    return result

Complexity: Time O(n), Space O(1) - since alphabet is bounded at 26 characters.


7.4 Sliding Window Maximum (LeetCode 239) - Advanced Fixed Window

Problem: Return the maximum value in each window of size k as it slides through the array.

Naive approach: O(n * k) - scan the entire window to find max at each step.

Sliding window approach: O(n) using a monotonic deque.

The Monotonic Deque Concept: A deque (double-ended queue) that maintains elements in decreasing order. The front always holds the maximum of the current window. When a new element enters:

  • Remove all elements from the back that are smaller than the new element (they can never be the maximum as long as the new element is in the window).
  • When the element at the front is out of the current window's range, remove it from the front.
Array: [1, 3, -1, -3, 5, 3, 6, 7],  k = 3

right=0: deque=[0]           (index 0, value 1)
right=1: 3 > 1, pop 0        deque=[1]           (index 1, value 3)
right=2: -1 < 3, push        deque=[1,2]         window=[0,2] max=arr[1]=3
right=3: -3 < -1, push       deque=[1,2,3]       window=[1,3] max=arr[1]=3
right=4: 5 > all, pop all    deque=[4]            window=[2,4] max=arr[4]=5
         arr[1] out of window (1 < right-k+1=2), already removed
right=5: 3 < 5, push         deque=[4,5]          window=[3,5] max=arr[4]=5
right=6: 6 > 3, pop 5; 6>5 pop 4 deque=[6]       window=[4,6] max=arr[6]=6
right=7: 7 > 6, pop 6        deque=[7]            window=[5,7] max=arr[7]=7

Output: [3, 3, 5, 5, 6, 7]

Java Implementation:

import java.util.*;
 
public class SlidingWindowMaximum {
 
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int[] result = new int[n - k + 1];
        // Deque stores indices; front = index of current window's max
        Deque<Integer> deque = new ArrayDeque<>();
 
        for (int right = 0; right < n; right++) {
            // Remove indices that are out of the current window
            while (!deque.isEmpty() && deque.peekFirst() < right - k + 1) {
                deque.pollFirst();
            }
 
            // Remove from the back all indices whose values are smaller
            // than the current element - they are useless candidates
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[right]) {
                deque.pollLast();
            }
 
            deque.offerLast(right);
 
            // Start recording results once the first full window is formed
            if (right >= k - 1) {
                result[right - k + 1] = nums[deque.peekFirst()];
            }
        }
 
        return result;
    }
}

Python Implementation:

from collections import deque
 
def max_sliding_window(nums: list[int], k: int) -> list[int]:
    result = []
    dq = deque()  # stores indices
 
    for right in range(len(nums)):
        # Remove out-of-window indices from front
        while dq and dq[0] < right - k + 1:
            dq.popleft()
 
        # Remove smaller elements from the back
        while dq and nums[dq[-1]] < nums[right]:
            dq.pop()
 
        dq.append(right)
 
        # Record result once first full window is complete
        if right >= k - 1:
            result.append(nums[dq[0]])
 
    return result

Complexity: Time O(n) amortized - each element enters and leaves the deque at most once. Space O(k).


8. Variable-Size Window Problems - Deep Dive

8.1 Longest Substring Without Repeating Characters (LeetCode 3)

Problem: Find the length of the longest substring with all unique characters.

Constraint: All characters in the window must be unique. When a duplicate enters, shrink from the left until the duplicate is removed.

Java Implementation:

import java.util.*;
 
public class LongestSubstringNoRepeat {
 
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> charIndex = new HashMap<>();
        int left = 0;
        int maxLength = 0;
 
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
 
            // If the character exists in the window, move left past its last occurrence
            // This collapses the while-loop shrink into a single jump
            if (charIndex.containsKey(c) && charIndex.get(c) >= left) {
                left = charIndex.get(c) + 1;
            }
 
            // Record the latest position of this character
            charIndex.put(c, right);
 
            // Update answer
            maxLength = Math.max(maxLength, right - left + 1);
        }
 
        return maxLength;
    }
}

Note on the optimization: Instead of shrinking one step at a time with a while loop, we directly jump left to lastIndex + 1. This works because we store the last seen index of each character and we know exactly where to jump. Both approaches are O(n); the jump approach has better constants.

Python Implementation:

def length_of_longest_substring(s: str) -> int:
    char_index = {}
    left = 0
    max_length = 0
 
    for right, c in enumerate(s):
        if c in char_index and char_index[c] >= left:
            left = char_index[c] + 1
 
        char_index[c] = right
        max_length = max(max_length, right - left + 1)
 
    return max_length

Complexity: Time O(n), Space O(min(n, alphabet_size))


8.2 Longest Substring with At Most K Distinct Characters (LeetCode 340)

Problem: Find the longest substring that contains at most k distinct characters.

Constraint: The number of distinct characters in the window must be <= k. The window is invalid when distinct count > k; shrink until valid.

Java Implementation:

import java.util.*;
 
public class LongestKDistinct {
 
    public int lengthOfLongestSubstringKDistinct(String s, int k) {
        if (k == 0) return 0;
 
        Map<Character, Integer> freq = new HashMap<>();
        int left = 0;
        int maxLength = 0;
 
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            freq.put(c, freq.getOrDefault(c, 0) + 1);
 
            // Contract: too many distinct characters
            while (freq.size() > k) {
                char leftChar = s.charAt(left);
                freq.put(leftChar, freq.get(leftChar) - 1);
                if (freq.get(leftChar) == 0) {
                    freq.remove(leftChar);
                }
                left++;
            }
 
            maxLength = Math.max(maxLength, right - left + 1);
        }
 
        return maxLength;
    }
}

Python Implementation:

from collections import defaultdict
 
def longest_k_distinct(s: str, k: int) -> int:
    if k == 0:
        return 0
 
    freq = defaultdict(int)
    left = 0
    max_length = 0
 
    for right, c in enumerate(s):
        freq[c] += 1
 
        while len(freq) > k:
            left_char = s[left]
            freq[left_char] -= 1
            if freq[left_char] == 0:
                del freq[left_char]
            left += 1
 
        max_length = max(max_length, right - left + 1)
 
    return max_length

8.3 Minimum Window Substring (LeetCode 76) - Hard

Problem: Given strings s and t, find the minimum window in s that contains all characters of t.

This is a landmark problem - it combines variable-size window with frequency matching and is a very common hard interview question.

Strategy:

  1. Build a frequency map of t.
  2. Expand the right pointer, adding characters to the window frequency map.
  3. Track how many characters are "formed" (have reached their required frequency).
  4. When all characters are formed (window is valid), record the window and try to shrink from the left.
  5. Continue until right reaches the end.

Java Implementation:

import java.util.*;
 
public class MinimumWindowSubstring {
 
    public String minWindow(String s, String t) {
        if (s.isEmpty() || t.isEmpty()) return "";
 
        // Required frequency of each character in t
        Map<Character, Integer> required = new HashMap<>();
        for (char c : t.toCharArray()) {
            required.put(c, required.getOrDefault(c, 0) + 1);
        }
 
        int totalRequired = required.size(); // number of unique chars needed
        int formed = 0; // number of unique chars in window meeting required frequency
 
        Map<Character, Integer> windowCounts = new HashMap<>();
        int left = 0;
        int minLen = Integer.MAX_VALUE;
        int minLeft = 0;
 
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            windowCounts.put(c, windowCounts.getOrDefault(c, 0) + 1);
 
            // Check if this character's frequency in the window
            // matches its required frequency
            if (required.containsKey(c) &&
                windowCounts.get(c).intValue() <mark class="obsidian-highlight"> required.get(c).intValue()) {
                formed++;
            }
 
            // Contract: try to shrink the window while it remains valid
            while (formed </mark> totalRequired && left <= right) {
                // Update the answer if this window is smaller
                if (right - left + 1 < minLen) {
                    minLen = right - left + 1;
                    minLeft = left;
                }
 
                // Remove the leftmost character
                char leftChar = s.charAt(left);
                windowCounts.put(leftChar, windowCounts.get(leftChar) - 1);
                if (required.containsKey(leftChar) &&
                    windowCounts.get(leftChar) < required.get(leftChar)) {
                    formed--;
                }
                left++;
            }
        }
 
        return minLen == Integer.MAX_VALUE ? "" : s.substring(minLeft, minLeft + minLen);
    }
}

Python Implementation:

from collections import Counter, defaultdict
 
def min_window(s: str, t: str) -> str:
    if not s or not t:
        return ""
 
    required = Counter(t)
    total_required = len(required)  # number of unique chars to satisfy
    formed = 0
 
    window_counts = defaultdict(int)
    left = 0
    min_len = float('inf')
    min_left = 0
 
    for right, c in enumerate(s):
        window_counts[c] += 1
 
        if c in required and window_counts[c] == required[c]:
            formed += 1
 
        while formed == total_required:
            # Update answer
            if right - left + 1 < min_len:
                min_len = right - left + 1
                min_left = left
 
            # Shrink from left
            left_char = s[left]
            window_counts[left_char] -= 1
            if left_char in required and window_counts[left_char] < required[left_char]:
                formed -= 1
            left += 1
 
    return "" if min_len == float('inf') else s[min_left:min_left + min_len]

8.4 Minimum Size Subarray Sum (LeetCode 209)

Problem: Find the minimum length of a subarray whose sum is >= target.

Java Implementation:

public class MinSizeSubarraySum {
 
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int currentSum = 0;
        int minLength = Integer.MAX_VALUE;
 
        for (int right = 0; right < nums.length; right++) {
            currentSum += nums[right];
 
            // As long as the window is valid, record and try to shrink
            while (currentSum >= target) {
                minLength = Math.min(minLength, right - left + 1);
                currentSum -= nums[left];
                left++;
            }
        }
 
        return minLength == Integer.MAX_VALUE ? 0 : minLength;
    }
}

Python Implementation:

def min_subarray_len(target: int, nums: list[int]) -> int:
    left = 0
    current_sum = 0
    min_length = float('inf')
 
    for right, num in enumerate(nums):
        current_sum += num
 
        while current_sum >= target:
            min_length = min(min_length, right - left + 1)
            current_sum -= nums[left]
            left += 1
 
    return 0 if min_length == float('inf') else min_length

8.5 Longest Repeating Character Replacement (LeetCode 424)

Problem: Given a string and a number k, find the longest substring where you can replace at most k characters to make all characters the same.

Key insight: A window is valid if (window_size - count_of_most_frequent_char) <= k. The number of replacements needed is the count of non-dominant characters.

Java Implementation:

public class LongestRepeatingReplacement {
 
    public int characterReplacement(String s, int k) {
        int[] freq = new int[26];
        int left = 0;
        int maxFreq = 0; // max frequency of any single character in the window
        int maxLength = 0;
 
        for (int right = 0; right < s.length(); right++) {
            int c = s.charAt(right) - 'A';
            freq[c]++;
            maxFreq = Math.max(maxFreq, freq[c]);
 
            // Check validity: replacements needed = window_size - maxFreq
            // If replacements needed > k, the window is invalid
            int windowSize = right - left + 1;
            if (windowSize - maxFreq > k) {
                // Shrink by one from the left
                freq[s.charAt(left) - 'A']--;
                left++;
                // Note: maxFreq may be stale but that is intentional and safe.
                // We never need to decrease maxFreq because a smaller window
                // with a lower maxFreq cannot give us a longer answer.
            }
 
            maxLength = Math.max(maxLength, right - left + 1);
        }
 
        return maxLength;
    }
}

Why we do not decrease maxFreq: This is a subtle but important optimization. We only care about finding a longer window than what we have already found. If the new window is not longer, we just slide it forward without updating maxFreq. We only update the answer when the window grows.

Python Implementation:

def character_replacement(s: str, k: int) -> int:
    freq = [0] * 26
    left = 0
    max_freq = 0
    max_length = 0
 
    for right, c in enumerate(s):
        idx = ord(c) - ord('A')
        freq[idx] += 1
        max_freq = max(max_freq, freq[idx])
 
        window_size = right - left + 1
        if window_size - max_freq > k:
            freq[ord(s[left]) - ord('A')] -= 1
            left += 1
 
        max_length = max(max_length, right - left + 1)
 
    return max_length

8.6 Subarrays with Product Less Than K (LeetCode 713)

Problem: Count subarrays whose product of all elements is strictly less than k.

Key insight: When the window [left, right] has a valid product (< k), every subarray ending at right and starting at or after left is also valid. There are (right - left + 1) such subarrays.

Java Implementation:

public class SubarraysProductLessK {
 
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) return 0;
 
        int left = 0;
        int product = 1;
        int count = 0;
 
        for (int right = 0; right < nums.length; right++) {
            product *= nums[right];
 
            while (product >= k) {
                product /= nums[left];
                left++;
            }
 
            // All subarrays [left, right], [left+1, right], ..., [right, right]
            // are valid. That is (right - left + 1) subarrays.
            count += right - left + 1;
        }
 
        return count;
    }
}

9. Advanced Sliding Window Techniques

9.1 The "Exactly K" Trick

Problem: Count subarrays with exactly k distinct elements.

Direct use of sliding window for "exactly k" is tricky because you cannot directly tell when to shrink. The elegant solution:

count(exactly k) = count(at most k) - count(at most k-1)

This converts an "exactly k" constraint into two "at most k" problems, each of which is solvable with a standard sliding window.

Java Implementation:

import java.util.*;
 
public class ExactlyKDistinct {
 
    public int subarraysWithKDistinct(int[] nums, int k) {
        return atMostK(nums, k) - atMostK(nums, k - 1);
    }
 
    private int atMostK(int[] nums, int k) {
        Map<Integer, Integer> freq = new HashMap<>();
        int left = 0;
        int count = 0;
 
        for (int right = 0; right < nums.length; right++) {
            freq.put(nums[right], freq.getOrDefault(nums[right], 0) + 1);
 
            while (freq.size() > k) {
                freq.put(nums[left], freq.get(nums[left]) - 1);
                if (freq.get(nums[left]) == 0) {
                    freq.remove(nums[left]);
                }
                left++;
            }
 
            count += right - left + 1;
        }
 
        return count;
    }
}

9.2 Sliding Window with Sorted Order - The Monotonic Deque Pattern

When you need order statistics (min, max, median) in a sliding window efficiently, a plain HashMap is insufficient. You need a structure that maintains sorted order as elements enter and exit.

Options:

  • Monotonic Deque: O(n) amortized for min/max only
  • TreeMap / Sorted Map: O(n log k) for any order statistic
  • Two Heaps: O(n log k) for median specifically

Sliding Window Median (LeetCode 480) using Two Heaps in Python:

import heapq
from collections import defaultdict
 
def median_sliding_window(nums: list[int], k: int) -> list[float]:
    # Max-heap (inverted) for the lower half
    # Min-heap for the upper half
    lo = []   # max-heap (negate values)
    hi = []   # min-heap
    lazy_del = defaultdict(int)  # lazy deletion tracker
 
    result = []
 
    def get_median():
        if k % 2 == 1:
            return float(hi[0])
        return (-lo[0] + hi[0]) / 2.0
 
    def balance():
        # Ensure hi has ceil(k/2) elements and lo has floor(k/2)
        while len(hi) > len(lo) + 1:
            heapq.heappush(lo, -heapq.heappop(hi))
        while len(lo) > len(hi):
            heapq.heappush(hi, -heapq.heappop(lo))
 
    def prune(heap, sign):
        # Remove lazily deleted elements from the top
        while heap and lazy_del[sign * heap[0]] > 0:
            lazy_del[sign * heap[0]] -= 1
            heapq.heappop(heap)
 
    # Build the first window
    for i in range(k):
        heapq.heappush(hi, nums[i])
 
    for _ in range(k // 2):
        heapq.heappush(lo, -heapq.heappop(hi))
 
    result.append(get_median())
 
    for right in range(k, len(nums)):
        incoming = nums[right]
        outgoing = nums[right - k]
 
        # Add incoming
        if incoming >= hi[0]:
            heapq.heappush(hi, incoming)
        else:
            heapq.heappush(lo, -incoming)
 
        # Lazily mark outgoing for deletion
        lazy_del[outgoing] += 1
 
        # Prune tops of heaps if they are marked for deletion
        prune(hi, 1)
        prune(lo, -1)
 
        balance()
        prune(hi, 1)
        prune(lo, -1)
 
        result.append(get_median())
 
    return result

9.3 Sliding Window in 2D (Matrix)

For 2D problems like "maximum sum rectangle of size k x k", reduce to 1D by compressing column sums using prefix sums for each column, then apply a 1D sliding window.


10. Real-World Applications Beyond Algorithms

The sliding window is not merely an academic construct. It permeates virtually every field of software engineering and computer science. Here is a comprehensive survey.

10.1 Rate Limiting Systems

Every API gateway, web server, and microservice uses sliding window rate limiting to control request throughput.

How it works:

  • A time-based sliding window (e.g., the last 60 seconds) is maintained per user/IP.
  • Each incoming request is added to the window.
  • Requests older than the window boundary are evicted.
  • If the count within the window exceeds the threshold, the request is rejected with HTTP 429 (Too Many Requests).

Fixed window vs. sliding window rate limiting:

Fixed window rate limiting resets the counter at fixed intervals (e.g., every minute on the clock). This creates a "double burst" vulnerability where a client can send 2x the allowed requests by sending them at the end of one window and the beginning of the next.

Sliding window rate limiting eliminates this by always evaluating the last N seconds from the current moment, preventing any burst exploitation.

Production implementations: AWS API Gateway, Kong, Nginx rate limiting module, Redis-based rate limiters (using sorted sets with timestamp scores as the sliding window).

10.2 Monitoring, Alerting, and Observability

Every monitoring system (Prometheus, Datadog, Grafana) computes metrics over a sliding time window:

  • Moving averages: CPU utilization averaged over the last 5 minutes
  • Error rate: HTTP 5xx errors per minute, computed over a rolling window
  • Percentiles: p99 latency computed over the last 15 minutes
  • Spike detection: Traffic anomaly when requests in the last 30 seconds exceed 3 standard deviations from the mean

The window slides forward in real-time, continuously evaluating the most recent data.


11. Circuit Breaker and Sliding Windows

The Circuit Breaker pattern (popularized by Netflix Hystrix and now used in Resilience4j, Polly, and others) is one of the most important distributed systems patterns, and its modern implementation is fundamentally built on a sliding window.

11.1 What is a Circuit Breaker?

The circuit breaker pattern prevents a failing downstream service from being bombarded with requests during an outage. Inspired by electrical circuit breakers, it has three states:

               Too many failures
CLOSED --------------------------> OPEN
  ^                                  |
  |                                  | After timeout
  |                                  v
  <---------- HALF-OPEN <-----------+
  (success)         (failure -> OPEN again)
  • CLOSED: Requests pass through. Failures are counted. Normal operation.
  • OPEN: All requests are immediately rejected (fail-fast). Service is assumed down.
  • HALF-OPEN: A limited probe request is let through to test if the service has recovered.

11.2 Netflix Hystrix - How It Actually Implements Its Sliding Window

Hystrix is where most engineers first encountered circuit breakers. But the sliding window Hystrix uses is not the naive per-call deque that most people imagine. Hystrix invented a smarter, production-hardened architecture called the rolling window with time buckets. Understanding it deeply is essential because it explains every Hystrix configuration property and why Resilience4j later refined the design.

11.2.1 The Core Problem Hystrix Was Solving

Netflix in 2012 was running hundreds of microservices. At their scale (tens of millions of concurrent users), a naive per-call counter had two problems:

  1. Memory: Storing every individual call result in a list for millions of concurrent requests across hundreds of services would consume enormous heap memory.
  2. Contention: A shared list with a lock that every request thread writes to becomes a concurrency bottleneck. At high throughput, this lock becomes the bottleneck of your entire application.

Hystrix solved both with a bucketed rolling window.

11.2.2 The Bucket Architecture - Hystrix's Actual Design

Instead of recording individual call outcomes, Hystrix divides time into fixed-width time buckets. Each bucket is a small data structure that accumulates aggregate counts for all calls that happened within that time slice.

Hystrix Default: 10 buckets, each 1 second wide = 10 second rolling window

Timeline (seconds):
  t=0      t=1      t=2      t=3     ...     t=9      t=10
  |        |        |        |               |        |
  [Bucket0][Bucket1][Bucket2][Bucket3]...[Bucket9]

Each Bucket stores:
  - successCount
  - failureCount
  - timeoutCount
  - rejectedCount (bulkhead/semaphore rejections)
  - shortCircuitedCount (requests rejected because circuit was OPEN)

At t=10, a new second begins:
  - New Bucket10 is created
  - Bucket0 (the oldest) is EVICTED from the rolling window
  - Window now covers t=1 through t=10

The window SLIDES forward by dropping the oldest bucket and adding a new one.

This is the key insight: Hystrix does not slide per-call, it slides per time-bucket. The granularity of the slide is one bucket width (default: 1 second), not one call.

11.2.3 Why Buckets Are Brilliant - The Engineering Tradeoffs

Memory efficiency: Instead of storing N individual call results (where N could be thousands per second), each bucket stores just a handful of integers regardless of how many calls happen within it.

Naive per-call window (10 seconds, 5000 calls/second):
  Memory = 10s * 5000 calls/s * (result object size) = 50,000 objects in heap

Hystrix bucket window (10 buckets):
  Memory = 10 buckets * (6 integers per bucket) = 60 integers
  Memory reduction: ~833x

Lock contention reduction: Each bucket has its own lock (or uses LongAdder / atomic fields internally). Threads writing to the current bucket do not block threads reading from older buckets. The lock scope is tiny.

Thread safety via HystrixRollingNumber: Hystrix's internal class HystrixRollingNumber implements the bucket window using an array of Bucket objects and an AtomicReference to the current bucket. Reads happen without locks by iterating the fixed-size array. Writes use CAS (Compare-And-Swap) operations on counters within the current bucket.

11.2.4 Complete Hystrix Configuration Properties - What Each One Controls

Understanding the sliding window means understanding exactly what every property configures:

HystrixCommandProperties.Setter()
    // THE ROLLING WINDOW
    .withMetricsRollingStatisticalWindowInMilliseconds(10000) // window size = 10 seconds
    .withMetricsRollingStatisticalWindowBuckets(10)           // number of buckets = 10
    // bucket width = 10000 / 10 = 1000ms per bucket
 
    // CIRCUIT BREAKER DECISION
    .withCircuitBreakerRequestVolumeThreshold(20)    // minimum calls in the window
                                                     // before the circuit CAN open
                                                     // (prevents opening on 1 failure out of 1 call)
    .withCircuitBreakerErrorThresholdPercentage(50)  // failure rate % to open the circuit
    .withCircuitBreakerSleepWindowInMilliseconds(5000) // how long to stay OPEN before
                                                       // trying HALF-OPEN probe
 
    // EXECUTION
    .withExecutionTimeoutInMilliseconds(1000)         // a timeout counts as a FAILURE
                                                      // in the rolling window
 
    // PERCENTILE WINDOW (separate from circuit breaker window - for latency metrics)
    .withMetricsRollingPercentileWindowInMilliseconds(60000) // 60 second window for latency
    .withMetricsRollingPercentileWindowBuckets(6)            // 6 buckets = 10 seconds each
    .withMetricsRollingPercentileEnabled(true)               // track p50, p75, p95, p99 latencies

Let us walk through what happens step by step with these defaults:

Scenario: 10-second window, 10 buckets, min 20 requests, 50% error threshold

Second 1:   5 successes,  0 failures  -> Bucket[0] = {S:5,  F:0}
Second 2:   5 successes,  0 failures  -> Bucket[1] = {S:5,  F:0}
Second 3:   3 successes,  2 failures  -> Bucket[2] = {S:3,  F:2}
Second 4:   0 successes,  5 failures  -> Bucket[3] = {S:0,  F:5}

Rolling window totals at end of second 4:
  Total calls    = (5+5+3+0) + (0+0+2+5) = 13 + 7 = 20 calls
  Total failures = 7
  Failure rate   = 7/20 = 35%
  -> 35% < 50% threshold AND calls(20) >= min(20) -> Circuit stays CLOSED

Second 5:   0 successes, 10 failures  -> Bucket[4] = {S:0, F:10}

Rolling window totals at end of second 5:
  Total calls    = 30
  Total failures = 17
  Failure rate   = 17/30 = 56.7%
  -> 56.7% > 50% threshold AND calls(30) >= min(20) -> CIRCUIT OPENS

Second 11:  Bucket[0] (second 1) slides out of the window.
            Now window covers seconds 2-11.
            Even if second 11 has 0 calls, the stale failure data from
            second 3 and 4 is still in the window.

This illustrates an important nuance of the bucket approach: data ages out at bucket granularity, not at per-second precision. A failure from second 3.999 and a failure from second 4.001 are in different buckets and age out one second apart. This is a deliberate tradeoff - the small inaccuracy is acceptable in exchange for the efficiency and thread-safety gains.

11.2.5 The requestVolumeThreshold - The Most Misunderstood Property

This property is critical and frequently misconfigured. Without it, the circuit breaker would open on trivially small samples:

Without requestVolumeThreshold:
  1 failure out of 1 call = 100% failure rate -> circuit opens immediately
  This is statistically meaningless and would cause constant unnecessary OPEN states

With requestVolumeThreshold = 20:
  1 failure out of 1 call = window has only 1 call, 1 < 20 -> circuit stays CLOSED
  Circuit can only open once at least 20 calls have been recorded in the window

Think of this as the minimum sample size for statistical significance. You would not declare a coin biased after one flip. You need a meaningful sample.

11.2.6 The Percentile Rolling Window - A Second, Separate Window in Hystrix

Hystrix actually maintains two separate rolling windows simultaneously:

  1. The circuit breaker window: Tracks success/failure/timeout counts. Default: 10 seconds, 10 buckets. Used to decide whether to open the circuit.

  2. The percentile window: Tracks individual execution latencies to compute p50, p75, p95, p99, p99.5 latency percentiles for monitoring and adaptive timeouts. Default: 60 seconds, 6 buckets (10 seconds per bucket). Does NOT affect circuit-opening decisions.

Percentile bucket at second t stores: a reservoir of latency values (up to 100 per bucket)
After each bucket rollover: percentiles are recomputed from the reservoir
Exposed via: HystrixCommandMetrics.getExecutionTimePercentile(99.0)

This is a more expensive structure (stores sampled latency values, not just counters) and is why Hystrix defaults to a larger 60-second window for percentiles - percentiles need more data points to be statistically meaningful.

11.2.7 Hystrix's Window vs. A True Sliding Window - The Key Difference

True per-call sliding window (like Resilience4j COUNT_BASED):
  Every call is individually tracked.
  Window slides by exactly one call when a new call is added.
  Accuracy: exact - every call in the window is accounted for.
  Memory: O(window_size) regardless of request rate.

Hystrix rolling window (bucket-based):
  Calls within the same time bucket are aggregated (summed).
  Window slides by one bucket width (default: 1 second) not by one call.
  Accuracy: approximate - sub-bucket precision is lost.
  Memory: O(num_buckets) regardless of request rate or window_size.
  Thread safety: better - bucket-level atomics, not window-level locks.

The bucket approach sacrifices a tiny amount of precision (sub-second accuracy) in exchange for dramatically better memory efficiency and concurrency.

11.2.8 What Type of Sliding Window Is Hystrix Using?

To directly answer the question:

Hystrix uses a time-based rolling window, but implemented via fixed-size time buckets (not a pure sliding window). It is more precisely called a "tumbling bucket approximation of a sliding window" or a "rolling aggregated window".

It is time-based (not count-based) because:

  • The window boundary is defined in milliseconds, not in number of calls.
  • The window slides forward as wall-clock time advances.
  • A bucket of 0 calls still ages out after its time width elapses.

It is NOT a pure sliding window because:

  • It does not slide continuously; it slides in discrete jumps of one bucket width (1 second default).
  • Data within a bucket is aggregated; individual call ordering is lost.

11.2.9 Building a Hystrix-Style Bucket Circuit Breaker From Scratch

The following implementations faithfully reproduce Hystrix's internal architecture:
the rolling bucket window, the requestVolumeThreshold gate, the error percentage
threshold, the three-state machine, and the sleep window. Reading and running this code
will make every Hystrix configuration property concrete and intuitive.

Java Implementation

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
 
/**
 * HystrixStyleCircuitBreaker
 *
 * Reproduces Hystrix's core internal design:
 *   - Rolling window divided into fixed-size time buckets
 *   - Each bucket aggregates call counts (success, failure, timeout)
 *   - Window slides by dropping the oldest bucket every bucketWidthMs
 *   - Circuit opens when: window is full AND failureRate >= threshold
 *   - Circuit re-probes after sleepWindowMs (HALF-OPEN)
 *
 * Default configuration mirrors Hystrix defaults:
 *   windowSizeMs    = 10000  (10 seconds)
 *   numBuckets      = 10     (1 second per bucket)
 *   minRequests     = 20     (requestVolumeThreshold)
 *   errorThreshold  = 50%    (circuitBreakerErrorThresholdPercentage)
 *   sleepWindowMs   = 5000   (circuitBreakerSleepWindowInMilliseconds)
 */
public class HystrixStyleCircuitBreaker {
 
    // -------------------------------------------------------------------------
    // A single time bucket - aggregates all calls within one time slice
    // Uses AtomicLong so multiple threads can write concurrently without locks
    // -------------------------------------------------------------------------
    static class Bucket {
        final long bucketStartMs;           // wall-clock start of this bucket
        final AtomicLong successCount  = new AtomicLong(0);
        final AtomicLong failureCount  = new AtomicLong(0);
        final AtomicLong timeoutCount  = new AtomicLong(0);
        // Total calls = success + failure + timeout
        // Failure rate = (failure + timeout) / total
 
        Bucket(long startMs) {
            this.bucketStartMs = startMs;
        }
 
        long total()    { return successCount.get() + failureCount.get() + timeoutCount.get(); }
        long failures() { return failureCount.get() + timeoutCount.get(); }
    }
 
    // -------------------------------------------------------------------------
    // The rolling window - a circular array of Bucket objects
    // -------------------------------------------------------------------------
    static class RollingWindow {
        private final Bucket[] buckets;      // circular array
        private final int      numBuckets;
        private final long     bucketWidthMs;
        private final long     windowSizeMs;
 
        // AtomicReference allows lock-free swap of the "current bucket" pointer
        private final AtomicReference<Bucket> currentBucketRef;
 
        RollingWindow(int numBuckets, long windowSizeMs) {
            this.numBuckets    = numBuckets;
            this.windowSizeMs  = windowSizeMs;
            this.bucketWidthMs = windowSizeMs / numBuckets;
            this.buckets       = new Bucket[numBuckets];
 
            long now = System.currentTimeMillis();
            // Pre-fill all slots with empty buckets so reads are never null
            for (int i = 0; i < numBuckets; i++) {
                buckets[i] = new Bucket(now - (numBuckets - i) * bucketWidthMs);
            }
            currentBucketRef = new AtomicReference<>(buckets[numBuckets - 1]);
        }
 
        /**
         * Returns the bucket that should receive the current call.
         * Creates a new bucket if the current second has rolled over.
         * This is the "slide" operation: old bucket is overwritten by new one.
         */
        Bucket getCurrentBucket() {
            long now      = System.currentTimeMillis();
            Bucket current = currentBucketRef.get();
 
            // Still within the same bucket's time slice - no slide needed
            if (now < current.bucketStartMs + bucketWidthMs) {
                return current;
            }
 
            // Time has moved past the current bucket - need to create new bucket(s)
            // and slide the window forward
            synchronized (this) {
                // Re-check inside the lock (another thread may have already slid)
                current = currentBucketRef.get();
                if (now < current.bucketStartMs + bucketWidthMs) {
                    return current;
                }
 
                // Calculate how many buckets we need to advance
                // (could be more than 1 if the system was idle for multiple seconds)
                long newBucketStartMs = current.bucketStartMs + bucketWidthMs;
                int  bucketsMissed    = (int) ((now - newBucketStartMs) / bucketWidthMs);
 
                // Fill any missed buckets with empty ones (no calls happened in that time)
                // This ensures stale data from a busy period does not persist
                for (int i = 0; i <= bucketsMissed; i++) {
                    long slotStart = newBucketStartMs + (i * bucketWidthMs);
                    int  slot      = computeSlot(slotStart);
 
                    // Overwrite the slot - this evicts the oldest data (the "slide")
                    buckets[slot] = new Bucket(slotStart);
                }
 
                // Point the current bucket reference at the newest slot
                long  newestStart  = newBucketStartMs + (bucketsMissed * bucketWidthMs);
                Bucket newCurrent  = buckets[computeSlot(newestStart)];
                currentBucketRef.set(newCurrent);
                return newCurrent;
            }
        }
 
        /**
         * Maps a bucket's start timestamp to its slot in the circular array.
         * Two buckets that are `windowSizeMs` apart map to the same slot,
         * which is why writing a new bucket naturally evicts the old one.
         */
        private int computeSlot(long bucketStartMs) {
            long bucketNumber = bucketStartMs / bucketWidthMs;
            return (int) (bucketNumber % numBuckets);
        }
 
        /**
         * Reads all buckets that fall within the current rolling window.
         * Buckets older than windowSizeMs are ignored.
         */
        WindowSnapshot snapshot() {
            long now         = System.currentTimeMillis();
            long windowStart = now - windowSizeMs;
 
            long totalCalls   = 0;
            long totalFails   = 0;
 
            for (Bucket b : buckets) {
                // Only count buckets that are within the rolling window
                if (b.bucketStartMs >= windowStart) {
                    totalCalls += b.total();
                    totalFails += b.failures();
                }
            }
            return new WindowSnapshot(totalCalls, totalFails);
        }
    }
 
    // Immutable snapshot of the window totals at a point in time
    record WindowSnapshot(long totalCalls, long totalFailures) {
        double failureRate() {
            return totalCalls == 0 ? 0.0 : (double) totalFailures / totalCalls * 100.0;
        }
    }
 
    // -------------------------------------------------------------------------
    // Circuit states
    // -------------------------------------------------------------------------
    enum State { CLOSED, OPEN, HALF_OPEN }
 
    // -------------------------------------------------------------------------
    // Circuit Breaker Configuration (mirrors Hystrix property names)
    // -------------------------------------------------------------------------
    record Config(
        int    numBuckets,              // metricsRollingStatisticalWindowBuckets      default: 10
        long   windowSizeMs,            // metricsRollingStatisticalWindowInMilliseconds default: 10000
        int    requestVolumeThreshold,  // circuitBreakerRequestVolumeThreshold         default: 20
        double errorThresholdPercent,   // circuitBreakerErrorThresholdPercentage       default: 50.0
        long   sleepWindowMs            // circuitBreakerSleepWindowInMilliseconds       default: 5000
    ) {
        static Config defaults() {
            return new Config(10, 10_000, 20, 50.0, 5_000);
        }
    }
 
    // -------------------------------------------------------------------------
    // Circuit Breaker State Machine
    // -------------------------------------------------------------------------
    private final Config        config;
    private final RollingWindow window;
 
    private volatile State  state      = State.CLOSED;
    private volatile long   openedAtMs = 0;
 
    public HystrixStyleCircuitBreaker(Config config) {
        this.config = config;
        this.window = new RollingWindow(config.numBuckets(), config.windowSizeMs());
    }
 
    /**
     * Call this BEFORE making the actual remote call.
     * Returns true if the request should be allowed through.
     * Returns false if the circuit is OPEN (fail-fast).
     */
    public boolean allowRequest() {
        if (state <mark class="obsidian-highlight"> State.CLOSED) {
            return true;
        }
 
        if (state </mark> State.OPEN) {
            long elapsed = System.currentTimeMillis() - openedAtMs;
            if (elapsed >= config.sleepWindowMs()) {
                // Sleep window has expired - allow one probe request (HALF-OPEN)
                state = State.HALF_OPEN;
                System.out.println("[CIRCUIT] State -> HALF_OPEN (probing recovery)");
                return true;
            }
            // Still within sleep window - reject immediately
            window.getCurrentBucket().failureCount.incrementAndGet(); // count as short-circuit
            return false;
        }
 
        // HALF_OPEN: allow the probe through
        return true;
    }
 
    /**
     * Call this AFTER the remote call succeeds.
     */
    public void recordSuccess() {
        window.getCurrentBucket().successCount.incrementAndGet();
 
        if (state <mark class="obsidian-highlight"> State.HALF_OPEN) {
            // Probe succeeded - the downstream service has recovered
            reset();
        }
    }
 
    /**
     * Call this AFTER the remote call fails (exception, non-2xx response, etc.)
     */
    public void recordFailure() {
        window.getCurrentBucket().failureCount.incrementAndGet();
        evaluateThreshold();
    }
 
    /**
     * Call this when the remote call times out.
     * Hystrix distinguishes timeouts from failures for metrics, but both trip the circuit.
     */
    public void recordTimeout() {
        window.getCurrentBucket().timeoutCount.incrementAndGet();
        evaluateThreshold();
    }
 
    /**
     * After every failure or timeout, check if the circuit should open.
     * Mirrors Hystrix's isOpen() logic exactly.
     */
    private void evaluateThreshold() {
        if (state </mark> State.HALF_OPEN) {
            // Probe failed - re-open the circuit
            tripBreaker();
            return;
        }
 
        if (state == State.OPEN) {
            return; // already open, nothing to evaluate
        }
 
        // CLOSED state: evaluate the rolling window
        WindowSnapshot snap = window.snapshot();
 
        // Gate 1: requestVolumeThreshold - do not open on tiny sample sizes
        if (snap.totalCalls() < config.requestVolumeThreshold()) {
            System.out.printf("[CIRCUIT] Calls in window: %d (below minimum %d) - staying CLOSED%n",
                snap.totalCalls(), config.requestVolumeThreshold());
            return;
        }
 
        // Gate 2: errorThresholdPercentage
        if (snap.failureRate() >= config.errorThresholdPercent()) {
            System.out.printf("[CIRCUIT] Failure rate: %.1f%% >= threshold %.1f%% - OPENING%n",
                snap.failureRate(), config.errorThresholdPercent());
            tripBreaker();
        }
    }
 
    private void tripBreaker() {
        state      = State.OPEN;
        openedAtMs = System.currentTimeMillis();
        System.out.println("[CIRCUIT] State -> OPEN");
    }
 
    private void reset() {
        state = State.CLOSED;
        // Hystrix does NOT clear the rolling window on reset.
        // The old failure data ages out naturally as time buckets expire.
        // This is intentional: if the service recovers but then fails again,
        // the circuit re-opens quickly because the failure history is still in the window.
        System.out.println("[CIRCUIT] State -> CLOSED (recovered)");
    }
 
    public State getState() { return state; }
 
    public WindowSnapshot getMetrics() { return window.snapshot(); }
 
    // -------------------------------------------------------------------------
    // Convenience: wrap a remote call with full circuit breaker protection
    // -------------------------------------------------------------------------
    @FunctionalInterface
    public interface RemoteCall<T> {
        T execute() throws Exception;
    }
 
    public <T> T execute(RemoteCall<T> call, T fallback) {
        if (!allowRequest()) {
            System.out.println("[CIRCUIT] Short-circuited - returning fallback");
            return fallback;
        }
 
        long startMs = System.currentTimeMillis();
        try {
            T result = call.execute();
            recordSuccess();
            return result;
        } catch (Exception e) {
            long elapsed = System.currentTimeMillis() - startMs;
            if (elapsed >= 1000) { // treat calls taking > 1s as timeouts
                recordTimeout();
            } else {
                recordFailure();
            }
            System.out.println("[CIRCUIT] Call failed: " + e.getMessage());
            return fallback;
        }
    }
 
    // -------------------------------------------------------------------------
    // Demo: simulate a service going down and recovering
    // -------------------------------------------------------------------------
    public static void main(String[] args) throws InterruptedException {
        HystrixStyleCircuitBreaker cb = new HystrixStyleCircuitBreaker(
            new Config(
                10,     // 10 buckets
                10_000, // 10-second window
                5,      // min 5 requests (lowered for demo)
                50.0,   // 50% failure threshold
                3_000   // 3-second sleep window (lowered for demo)
            )
        );
 
        System.out.println("=<mark class="obsidian-highlight"> Phase 1: Healthy service (should stay CLOSED) </mark>=");
        for (int i = 0; i < 6; i++) {
            cb.execute(() -> "OK", "FALLBACK");
        }
        System.out.println("State: " + cb.getState());
 
        System.out.println("\n=<mark class="obsidian-highlight"> Phase 2: Service degrades - 70% failures </mark>=");
        for (int i = 0; i < 10; i++) {
            if (i < 7) {
                // Simulate failure
                cb.execute(() -> { throw new RuntimeException("Connection refused"); }, "FALLBACK");
            } else {
                cb.execute(() -> "OK", "FALLBACK");
            }
        }
        System.out.println("State: " + cb.getState());
        WindowSnapshot snap = cb.getMetrics();
        System.out.printf("Window: %d total calls, %.1f%% failure rate%n",
            snap.totalCalls(), snap.failureRate());
 
        System.out.println("\n=<mark class="obsidian-highlight"> Phase 3: Circuit is OPEN - all calls short-circuited </mark>=");
        for (int i = 0; i < 5; i++) {
            String result = cb.execute(() -> "OK", "FALLBACK");
            System.out.println("Result: " + result);
        }
 
        System.out.println("\n=<mark class="obsidian-highlight"> Phase 4: Wait for sleep window, then probe (HALF-OPEN) </mark>=");
        System.out.println("Waiting 3 seconds for sleep window...");
        Thread.sleep(3_100);
 
        System.out.println("Service has recovered - probe should succeed:");
        String result = cb.execute(() -> "RECOVERED", "FALLBACK");
        System.out.println("Probe result: " + result);
        System.out.println("State after recovery: " + cb.getState());
    }
}

Expected output:

=<mark class="obsidian-highlight"> Phase 1: Healthy service (should stay CLOSED) </mark>=
[CIRCUIT] Calls in window: 1 (below minimum 5) - staying CLOSED
... (5 more times)
State: CLOSED

=<mark class="obsidian-highlight"> Phase 2: Service degrades - 70% failures </mark>=
[CIRCUIT] Call failed: Connection refused
...
[CIRCUIT] Failure rate: 70.0% >= threshold 50.0% - OPENING
[CIRCUIT] State -> OPEN
State: OPEN
Window: 16 total calls, 43.8% failure rate

=<mark class="obsidian-highlight"> Phase 3: Circuit is OPEN - all calls short-circuited </mark>=
[CIRCUIT] Short-circuited - returning fallback
Result: FALLBACK
... (4 more times)

=<mark class="obsidian-highlight"> Phase 4: Wait for sleep window, then probe (HALF-OPEN) </mark>=
Waiting 3 seconds for sleep window...
Service has recovered - probe should succeed:
[CIRCUIT] State -> HALF_OPEN (probing recovery)
[CIRCUIT] State -> CLOSED (recovered)
Probe result: RECOVERED
State after recovery: CLOSED

Python Implementation

"""
hystrix_circuit_breaker.py
===========================
A faithful reproduction of Hystrix's internal bucket-based rolling window
circuit breaker in Python. Every design decision mirrors the Java original:
 
  - Time bucketed rolling window (not per-call)
  - Atomic-style thread-safe bucket writes (via threading.Lock per bucket)
  - requestVolumeThreshold gate before evaluating error rate
  - Three-state machine: CLOSED -> OPEN -> HALF_OPEN -> CLOSED
  - Stale buckets are NOT cleared on reset (ages out naturally)
"""
 
import time
import threading
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, TypeVar, Optional
 
T = TypeVar('T')
 
 
# ===========================================================================<mark class="obsidian-highlight">
# Configuration - property names mirror Hystrix exactly
# </mark>===========================================================================
 
@dataclass(frozen=True)
class HystrixConfig:
    """
    All times in seconds for readability (Hystrix uses milliseconds internally).
    """
    metrics_rolling_statistical_window_seconds: float = 10.0
    metrics_rolling_statistical_window_buckets: int   = 10
    circuit_breaker_request_volume_threshold: int     = 20
    circuit_breaker_error_threshold_percentage: float = 50.0
    circuit_breaker_sleep_window_seconds: float       = 5.0
 
    @property
    def bucket_width_seconds(self) -> float:
        return (self.metrics_rolling_statistical_window_seconds /
                self.metrics_rolling_statistical_window_buckets)
 
 
# ===========================================================================<mark class="obsidian-highlight">
# Time Bucket - one time slice in the rolling window
# </mark>===========================================================================
 
class Bucket:
    """
    Aggregates all call outcomes for a fixed time slice.
    Thread-safe: uses a reentrant lock for counter updates.
    In production this would use atomics; Python's GIL gives some safety
    but explicit locking makes the intent clear.
    """
 
    def __init__(self, start_time: float):
        self.start_time: float = start_time
        self._lock = threading.Lock()
        self.success_count: int = 0
        self.failure_count: int = 0
        self.timeout_count: int = 0
 
    def record_success(self):
        with self._lock:
            self.success_count += 1
 
    def record_failure(self):
        with self._lock:
            self.failure_count += 1
 
    def record_timeout(self):
        with self._lock:
            self.timeout_count += 1
 
    @property
    def total(self) -> int:
        return self.success_count + self.failure_count + self.timeout_count
 
    @property
    def failures(self) -> int:
        # Hystrix counts both failures AND timeouts against the error rate
        return self.failure_count + self.timeout_count
 
    def __repr__(self) -> str:
        return (f"Bucket(t={self.start_time:.1f}, "
                f"S={self.success_count}, F={self.failure_count}, "
                f"T={self.timeout_count})")
 
 
# ===========================================================================<mark class="obsidian-highlight">
# Rolling Window - circular array of Buckets
# </mark>=========================================================================<mark class="obsidian-highlight">
 
@dataclass
class WindowSnapshot:
    total_calls: int
    total_failures: int
 
    @property
    def failure_rate(self) -> float:
        if self.total_calls </mark> 0:
            return 0.0
        return (self.total_failures / self.total_calls) * 100.0
 
 
class RollingWindow:
    """
    Hystrix-style rolling window implemented as a circular array of Buckets.
 
    Key properties:
      - numBuckets slots in a circular array.
      - Each slot represents one time slice of width `bucket_width_seconds`.
      - The "current" slot is determined by wall-clock time.
      - When time advances past the current bucket, new empty buckets are
        written into the circular array, naturally evicting old data.
        This is the "slide" operation.
    """
 
    def __init__(self, config: HystrixConfig):
        self._config  = config
        self._n       = config.metrics_rolling_statistical_window_buckets
        self._width   = config.bucket_width_seconds
        self._window  = config.metrics_rolling_statistical_window_seconds
        self._lock    = threading.Lock()
 
        # Pre-populate circular array with empty buckets
        now = time.time()
        self._buckets: list[Bucket] = [
            Bucket(now - (self._n - i) * self._width)
            for i in range(self._n)
        ]
        self._current_bucket: Bucket = self._buckets[-1]
 
    def _slot_for(self, bucket_start: float) -> int:
        """
        Maps a bucket's start time to its position in the circular array.
        Two bucket starts that are `window_seconds` apart map to the same slot,
        meaning a new bucket naturally evicts the old one at that position.
        """
        bucket_number = int(bucket_start / self._width)
        return bucket_number % self._n
 
    def get_current_bucket(self) -> Bucket:
        """
        Returns the bucket for the current moment in time.
        If the clock has moved past the current bucket, creates new buckets
        (overwriting stale ones in the circular array) and slides the window.
        """
        now = time.time()
 
        # Fast path: still within the current bucket's time slice
        if now < self._current_bucket.start_time + self._width:
            return self._current_bucket
 
        # Slow path: time has advanced, need to slide the window
        with self._lock:
            # Re-check after acquiring lock (another thread may have slid already)
            if now < self._current_bucket.start_time + self._width:
                return self._current_bucket
 
            new_start      = self._current_bucket.start_time + self._width
            buckets_missed = int((now - new_start) / self._width)
 
            # Fill in any missed buckets with empty ones
            # (handles the case where the system was idle for several seconds)
            for i in range(buckets_missed + 1):
                slot_start = new_start + i * self._width
                slot       = self._slot_for(slot_start)
                # SLIDE: overwriting this slot evicts the bucket that was here
                # (which is exactly `window_seconds` in the past)
                self._buckets[slot] = Bucket(slot_start)
 
            # Point current bucket at the newest slot
            newest_start          = new_start + buckets_missed * self._width
            newest_slot           = self._slot_for(newest_start)
            self._current_bucket  = self._buckets[newest_slot]
            return self._current_bucket
 
    def snapshot(self) -> WindowSnapshot:
        """
        Reads all buckets within the rolling window and returns aggregate totals.
        Buckets older than `window_seconds` are excluded.
        """
        window_start  = time.time() - self._window
        total_calls   = 0
        total_failures = 0
 
        for bucket in self._buckets:
            if bucket.start_time >= window_start:
                total_calls    += bucket.total
                total_failures += bucket.failures
 
        return WindowSnapshot(total_calls, total_failures)
 
 
# ===========================================================================<mark class="obsidian-highlight">
# Circuit Breaker State Machine
# </mark>===========================================================================
 
class CircuitState(Enum):
    CLOSED    = "CLOSED"
    OPEN      = "OPEN"
    HALF_OPEN = "HALF_OPEN"
 
 
class HystrixCircuitBreaker:
    """
    Hystrix-style circuit breaker with rolling bucket window.
 
    Usage:
        cb = HystrixCircuitBreaker(HystrixConfig(
            metrics_rolling_statistical_window_seconds=10,
            metrics_rolling_statistical_window_buckets=10,
            circuit_breaker_request_volume_threshold=20,
            circuit_breaker_error_threshold_percentage=50.0,
            circuit_breaker_sleep_window_seconds=5.0
        ))
 
        if cb.allow_request():
            try:
                result = call_remote_service()
                cb.record_success()
            except Exception as e:
                cb.record_failure()
        else:
            result = fallback_value
    """
 
    def __init__(self, config: HystrixConfig = HystrixConfig()):
        self._config     = config
        self._window     = RollingWindow(config)
        self._state      = CircuitState.CLOSED
        self._opened_at: Optional[float] = None
        self._state_lock = threading.Lock()
 
    @property
    def state(self) -> CircuitState:
        return self._state
 
    def allow_request(self) -> bool:
        """
        Gate: should this request be allowed through?
        Called BEFORE making the remote call.
        """
        if self._state <mark class="obsidian-highlight"> CircuitState.CLOSED:
            return True
 
        if self._state </mark> CircuitState.OPEN:
            elapsed = time.time() - self._opened_at
            if elapsed >= self._config.circuit_breaker_sleep_window_seconds:
                with self._state_lock:
                    # Double-check pattern (another thread may have transitioned already)
                    if self._state == CircuitState.OPEN:
                        self._state = CircuitState.HALF_OPEN
                        print("[CIRCUIT] State -> HALF_OPEN (probing recovery)")
                return True
            # Still within sleep window: short-circuit the request
            return False
 
        # HALF_OPEN: allow the single probe through
        return True
 
    def record_success(self):
        """Call this after a successful remote call."""
        self._window.get_current_bucket().record_success()
 
        if self._state <mark class="obsidian-highlight"> CircuitState.HALF_OPEN:
            self._reset()
 
    def record_failure(self):
        """Call this after a failed remote call (exception or error response)."""
        self._window.get_current_bucket().record_failure()
        self._evaluate()
 
    def record_timeout(self):
        """
        Call this when the remote call exceeds the configured timeout.
        Hystrix tracks timeouts separately from failures in metrics,
        but both contribute equally to the error rate.
        """
        self._window.get_current_bucket().record_timeout()
        self._evaluate()
 
    def get_metrics(self) -> WindowSnapshot:
        """Returns a snapshot of current rolling window statistics."""
        return self._window.snapshot()
 
    def _evaluate(self):
        """
        Mirrors Hystrix's isOpen() calculation.
        Only trips the circuit from CLOSED state.
        From HALF_OPEN, any failure immediately re-opens.
        """
        if self._state </mark> CircuitState.HALF_OPEN:
            print("[CIRCUIT] Probe failed - re-opening circuit")
            self._trip()
            return
 
        if self._state == CircuitState.OPEN:
            return  # already open, nothing to do
 
        # Evaluate from CLOSED state
        snap = self._window.snapshot()
 
        # Gate 1: requestVolumeThreshold
        # Do not open on a tiny or unrepresentative sample
        if snap.total_calls < self._config.circuit_breaker_request_volume_threshold:
            print(f"[CIRCUIT] {snap.total_calls} calls in window "
                  f"(below minimum {self._config.circuit_breaker_request_volume_threshold})"
                  f" - staying CLOSED")
            return
 
        # Gate 2: errorThresholdPercentage
        if snap.failure_rate >= self._config.circuit_breaker_error_threshold_percentage:
            print(f"[CIRCUIT] Failure rate {snap.failure_rate:.1f}% "
                  f">= threshold {self._config.circuit_breaker_error_threshold_percentage}% "
                  f"- OPENING")
            self._trip()
 
    def _trip(self):
        with self._state_lock:
            self._state     = CircuitState.OPEN
            self._opened_at = time.time()
        print("[CIRCUIT] State -> OPEN")
 
    def _reset(self):
        with self._state_lock:
            self._state = CircuitState.CLOSED
            # INTENTIONAL: do NOT clear the rolling window here.
            # Old failure data ages out naturally as buckets expire.
            # Keeping it means the circuit re-opens quickly if recovery is partial.
        print("[CIRCUIT] State -> CLOSED (recovered)")
 
    def execute(self, call: Callable[[], T], fallback: T,
                timeout_seconds: float = 1.0) -> T:
        """
        Convenience wrapper: checks circuit, calls the function,
        records the outcome, and returns either the real result or the fallback.
        """
        if not self.allow_request():
            print("[CIRCUIT] Short-circuited - returning fallback")
            return fallback
 
        start = time.time()
        try:
            result = call()
            self.record_success()
            return result
        except Exception as e:
            elapsed = time.time() - start
            if elapsed >= timeout_seconds:
                self.record_timeout()
                print(f"[CIRCUIT] Timeout after {elapsed:.1f}s: {e}")
            else:
                self.record_failure()
                print(f"[CIRCUIT] Failure: {e}")
            return fallback
 
 
# ===========================================================================<mark class="obsidian-highlight">
# Demo: simulate a service going down and recovering
# </mark>===========================================================================
 
def demo():
    config = HystrixConfig(
        metrics_rolling_statistical_window_seconds=10,
        metrics_rolling_statistical_window_buckets=10,
        circuit_breaker_request_volume_threshold=5,   # lowered for demo
        circuit_breaker_error_threshold_percentage=50.0,
        circuit_breaker_sleep_window_seconds=3.0,     # lowered for demo
    )
    cb = HystrixCircuitBreaker(config)
 
    print("=" * 60)
    print("Phase 1: Healthy service (should stay CLOSED)")
    print("=" * 60)
    for _ in range(6):
        cb.execute(lambda: "OK", fallback="FALLBACK")
    snap = cb.get_metrics()
    print(f"State: {cb.state.value}  |  "
          f"Calls: {snap.total_calls}  |  "
          f"Failure rate: {snap.failure_rate:.1f}%\n")
 
    print("=" * 60)
    print("Phase 2: Service degrades (7 failures, 3 successes)")
    print("=" * 60)
    for i in range(10):
        if i < 7:
            cb.execute(lambda: (_ for _ in ()).throw(ConnectionError("refused")),
                       fallback="FALLBACK")
        else:
            cb.execute(lambda: "OK", fallback="FALLBACK")
    snap = cb.get_metrics()
    print(f"State: {cb.state.value}  |  "
          f"Calls: {snap.total_calls}  |  "
          f"Failure rate: {snap.failure_rate:.1f}%\n")
 
    print("=" * 60)
    print("Phase 3: Circuit OPEN - all calls short-circuited")
    print("=" * 60)
    for _ in range(5):
        result = cb.execute(lambda: "OK", fallback="FALLBACK")
        print(f"  Result: {result}")
    print()
 
    print("=" * 60)
    print("Phase 4: Sleep window expires - probe with HALF_OPEN")
    print("=" * 60)
    print("Sleeping 3.1 seconds for sleep window to expire...")
    time.sleep(3.1)
 
    print("Service has recovered - sending probe:")
    result = cb.execute(lambda: "RECOVERED", fallback="FALLBACK")
    print(f"Probe result: {result}")
    print(f"Final state: {cb.state.value}")
 
 
if __name__ == "__main__":
    demo()

Key design decisions to notice in both implementations:

DecisionWhy Hystrix Made This Choice
Circular array of bucketsO(1) write to current bucket; O(n_buckets) read across window - n is tiny (10)
AtomicLong / lock-per-bucketThreads write to current bucket concurrently without blocking reads of other buckets
computeSlot via modulo on bucket numberWriting a new bucket automatically overwrites the bucket from windowSize seconds ago
Do NOT clear window on resetPartial recovery is detected quickly - failure history remains valid
Evaluate AFTER recording, not beforeThe call that trips the threshold is also counted in the window
requestVolumeThreshold checked firstAvoids false trips during cold start or low-traffic periods

11.2.10 Circuit Breaker in a Multithreaded Environment - Concurrency Deep Dive

This is the section most tutorials skip entirely, yet it is the section that determines whether a circuit breaker is actually correct in production — because in every real server, a circuit breaker is a shared singleton accessed simultaneously by hundreds or thousands of threads.

The Fundamental Concurrency Problem

In a web application, a CircuitBreaker instance lives as a singleton in the application context (Spring Bean, static field, dependency-injected singleton). Every incoming HTTP request runs in its own thread. All those threads share the one circuit breaker object.

Thread-1 (Request A): cb.allowRequest() -> true  ---> calls downstream
Thread-2 (Request B): cb.allowRequest() -> true  ---> calls downstream
Thread-3 (Request C): cb.allowRequest() -> true  ---> calls downstream
Thread-4 (Request D): cb.allowRequest() -> true  ---> calls downstream
         ... 200 more concurrent threads doing the same thing ...
Thread-204 (Request X): cb.recordFailure()  <--- modifying shared state
Thread-205 (Request Y): cb.recordFailure()  <--- modifying shared state simultaneously

Without careful design, this leads to race conditions at every layer of the circuit breaker. There are four distinct concurrency hazards.


Hazard 1: The Check-Then-Act Race on allowRequest()

Consider this sequence with two threads and a broken naive circuit breaker:

                Thread-1                           Thread-2
                --------                           --------
1.  reads state = CLOSED                  reads state = CLOSED
2.  decides: "allowed, proceed"           decides: "allowed, proceed"
3.  calls downstream service              calls downstream service
4.  service returns error                 service returns error
5.  records failure (count = 1)
6.                                        records failure (count = 2)
7.  reads window: 2/5 failures = 40%
8.                                        reads window: 2/5 failures = 40%
    (both threads see same stale snapshot)
9.  evaluates: 40% < 50% threshold        evaluates: 40% < 50% threshold
    -> does NOT trip circuit              -> does NOT trip circuit

This is benign here, but consider when BOTH threads are the ones that push the failure rate OVER the threshold:

                Thread-1                           Thread-2
                --------                           --------
1.  records failure -> window: 9/10 = 90%
    (just crossed threshold)
2.                                        records failure -> window: 9/10 = 90%
                                          (reads same in-flight state)
3.  _trip() -> sets state = OPEN          _trip() -> sets state = OPEN
    openedAt = t1                         openedAt = t2   <- CLOBBERS t1

Both threads call _trip(). This is a benign duplicate in this case (both set OPEN), but openedAt gets clobbered with a newer timestamp, making the sleep window slightly shorter than intended. In worse designs, it can lead to state corruption.

The fix: Use a Compare-And-Swap (CAS) for state transitions so only ONE thread wins the race:

// WRONG: not atomic
if (state != State.OPEN) {
    state = State.OPEN;
    openedAt = System.currentTimeMillis();
}
 
// CORRECT: CAS - only the first thread to swap wins
private final AtomicReference<State> stateRef = new AtomicReference<>(State.CLOSED);
 
// Only one thread will successfully compare-and-swap from CLOSED to OPEN
if (stateRef.compareAndSet(State.CLOSED, State.OPEN)) {
    openedAt = System.currentTimeMillis();
    // Only the winning thread reaches here
}

Hazard 2: The HALF_OPEN Thundering Herd Problem

This is the most damaging race condition. When the sleep window expires and the circuit transitions to HALF_OPEN, the intent is to let through exactly one probe request to test recovery. Without synchronization:

Sleep window expires at t = 100

Thread-1: allowRequest() -> sees OPEN + elapsed >= sleepWindow -> sets HALF_OPEN -> returns true
Thread-2: allowRequest() -> sees OPEN + elapsed >= sleepWindow -> sets HALF_OPEN -> returns true
Thread-3: allowRequest() -> sees HALF_OPEN -> returns true
Thread-4: allowRequest() -> sees HALF_OPEN -> returns true
...
Thread-100: allowRequest() -> sees HALF_OPEN -> returns true

Result: 100 simultaneous probe requests hit the recovering service
        A service that was barely recovering now gets 100x load simultaneously
        It collapses again -> all 100 record failures -> circuit opens again
        The service is permanently thrashing between OPEN and failing HALF_OPEN

This is called the thundering herd or probe stampede. It can prevent a service from ever recovering.

How Hystrix solves it: Hystrix uses an AtomicBoolean called circuitOpen combined with the fact that HystrixCircuitBreaker.isOpen() only allows one thread through during HALF_OPEN. The logic uses a try-lock pattern:

// Hystrix's actual HALF_OPEN probe gate
private final AtomicLong circuitOpenedOrLastTestedTime = new AtomicLong();
 
public boolean allowSingleTest() {
    long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get();
    long now = System.currentTimeMillis();
 
    if (circuitOpen.get()
        && now > timeCircuitOpenedOrWasLastTested + sleepWindowMs) {
        // Use CAS: only ONE thread successfully updates the timestamp
        // The thread that wins gets to send the probe
        // All other concurrent threads see a "fresh" timestamp and get rejected
        if (circuitOpenedOrLastTestedTime.compareAndSet(
                timeCircuitOpenedOrWasLastTested, now)) {
            return true;  // THIS thread gets to probe
        }
    }
    return false;  // all other concurrent threads are rejected
}

The compareAndSet is the lock-free gate. Only the first thread to win the CAS gets through. Every other thread that hits allowRequest() at the same millisecond loses the CAS, sees the updated timestamp, and gets rejected.

Resilience4j's solution: Uses an AtomicInteger permittedNumberOfCallsInHalfOpenState counter, decremented atomically. When it hits zero, no more probes are allowed:

// Resilience4j's HALF_OPEN probe control
private final AtomicInteger allowedCalls;
 
boolean tryPermitCall() {
    // getAndDecrement returns old value; if old value was > 0, we got a permit
    int allowed = allowedCalls.getAndDecrement();
    return allowed > 0;
}

Hazard 3: Rolling Window Counter Race (Write-Write Conflict)

When two threads simultaneously try to record a call result into the current bucket, without atomics they can lose increments:

Thread-1 reads failureCount = 42
Thread-2 reads failureCount = 42   <- both read the same stale value
Thread-1 writes failureCount = 43
Thread-2 writes failureCount = 43  <- OVERWRITES Thread-1's write
                                    -> lost increment: count should be 44

Hystrix's fix: HystrixRollingNumber uses LongAdder (Java 8+) for bucket counters. LongAdder is specifically designed for high-concurrency accumulation — it maintains multiple internal cells, one per CPU core, and only combines them on read. This eliminates write-write contention entirely:

// Java 8+ - no contention at any thread count
import java.util.concurrent.atomic.LongAdder;
 
class Bucket {
    final LongAdder successCount = new LongAdder();
    final LongAdder failureCount = new LongAdder();
    final LongAdder timeoutCount = new LongAdder();
 
    // Thread-safe increment - no locking, no CAS retry loop
    void recordSuccess() { successCount.increment(); }
    void recordFailure() { failureCount.increment(); }
    void recordTimeout() { timeoutCount.increment(); }
 
    // sum() combines all internal cells - only called on reads (infrequent)
    long total()    { return successCount.sum() + failureCount.sum() + timeoutCount.sum(); }
    long failures() { return failureCount.sum() + timeoutCount.sum(); }
}

Hazard 4: Bucket Rollover Race (Sliding the Window)

When the current bucket's time slice expires and multiple threads simultaneously try to create the new bucket:

At t=1001ms (bucket 0 expires):

Thread-1: getCurrentBucket() -> detects bucket is stale -> creates Bucket[1] at slot 1
Thread-2: getCurrentBucket() -> also detects bucket is stale -> creates ANOTHER Bucket[1] at slot 1
Thread-3: getCurrentBucket() -> same -> creates YET ANOTHER Bucket[1] at slot 1

Result: 3 different Bucket objects at the same slot. Threads write to different objects.
        Data is split across 3 buckets. Reads aggregate the wrong object. Counts are wrong.

Hystrix's fix: The rollover is protected by a synchronized block, but only around the creation step — not around the reads or counter increments. This is the double-checked locking pattern:

Bucket getCurrentBucket() {
    long now = System.currentTimeMillis();
    Bucket current = currentBucketRef.get();  // lock-free fast path
 
    // Fast path: still in the same bucket (99.9% of calls take this path)
    if (now < current.startTime + bucketWidthMs) {
        return current;
    }
 
    // Slow path: bucket needs rolling - synchronize to prevent duplicate creation
    synchronized (newBucketLock) {
        current = currentBucketRef.get();  // re-read inside lock
        if (now < current.startTime + bucketWidthMs) {
            return current; // another thread already created the new bucket
        }
 
        // Only ONE thread creates the new bucket
        Bucket newBucket = new Bucket(now);
        int slot = computeSlot(now);
        buckets[slot] = newBucket;
        currentBucketRef.set(newBucket); // make it visible to all threads atomically
        return newBucket;
    }
}

The synchronized block is taken extremely rarely (once per bucket interval = once per second by default). The fast path — the path taken by 99.9% of calls — is completely lock-free.


Hystrix's Full Thread Architecture: Two Completely Separate Isolation Strategies

Beyond the circuit breaker's internal thread safety, Hystrix introduced a broader thread model that many engineers miss entirely. There are two distinct isolation mechanisms in Hystrix. They solve different problems.

Every HystrixCommand type has its own dedicated HystrixThreadPool. When a request calls a Hystrix-protected method, the actual remote call runs in a thread from that pool, not in the caller's thread.

HTTP Request Thread (Tomcat thread pool - caller)
        |
        | calls
        v
HystrixCommand.execute()
        |
        | submits to
        v
HystrixThreadPool (separate pool, dedicated to "PaymentService" commands)
        |
        | runs in
        v
Remote Call to PaymentService --> response or timeout
        |
        | result returned to
        v
HTTP Request Thread (caller unblocked, received Future result)

Why this matters:

  1. Timeout enforcement: The HTTP request thread submits the work and waits with a timeout. If the downstream call takes longer than executionTimeoutInMilliseconds, the HTTP thread stops waiting and records a timeout — the remote call thread is abandoned (not cancelled, but abandoned). The caller is never stuck.

  2. Thread pool exhaustion = bulkhead: If PaymentService is slow and all 10 threads in its pool are occupied, the 11th caller gets a RejectedExecutionException immediately, without waiting. This is the bulkhead effect — PaymentService's slowness cannot steal threads from other services' pools.

  3. Complete caller isolation: The caller thread is free the moment it submits work. It can do other things, serve other requests, or apply a fallback while the remote call is in-flight.

// Hystrix thread pool configuration per command
HystrixThreadPoolProperties.Setter()
    .withCoreSize(10)              // 10 threads dedicated to this command type
    .withMaximumSize(10)           // max 10 (set allowMaximumSizeToDivergeFromCoreSize for dynamic)
    .withMaxQueueSize(5)           // allow 5 requests to queue before rejecting
    .withQueueSizeRejectionThreshold(5)  // reject at this queue depth (even if maxQueueSize is larger)
    .withKeepAliveTimeMinutes(1)   // idle thread survival time

The cost of thread pool isolation: Each thread switch involves a context switch. At very high throughput (100,000+ req/sec), the overhead of thread pool submission adds measurable latency (~1-3ms per call). This is why Hystrix offers semaphore isolation as an alternative.

Semaphore Isolation (Alternative for Low-Latency Commands)

Instead of a separate thread pool, semaphore isolation uses an AtomicInteger counter to limit concurrency. The command runs in the caller's thread — no thread hand-off:

HTTP Request Thread
        |
        | acquires semaphore (AtomicInteger concurrentCalls.incrementAndGet())
        | if count > maxConcurrentRequests -> reject immediately
        v
Remote Call (runs IN the HTTP request thread)
        |
        | on completion (success or exception)
        v
HTTP Request Thread releases semaphore (AtomicInteger.decrementAndGet())

Tradeoff table:

DimensionThread Pool IsolationSemaphore Isolation
Timeout enforcementYes - caller waits on Future with timeoutNo - caller thread is the executing thread; JVM cannot interrupt a blocking IO call cleanly
Context switch overheadYes (submit to pool)None
Memory overheadYes (pool threads)Minimal (one AtomicInteger)
Bulkhead isolationStrong (own thread pool)Weak (shares caller thread pool)
Best forRemote network calls (I/O bound, can timeout)In-process caches, very fast non-blocking calls
// Hystrix semaphore isolation configuration
HystrixCommandProperties.Setter()
    .withExecutionIsolationStrategy(
        HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
    .withExecutionIsolationSemaphoreMaxConcurrentRequests(20) // at most 20 concurrent callers

The Bulkhead Pattern - Sliding Window on Concurrency

The Bulkhead pattern is the concurrency complement to the circuit breaker. While the circuit breaker uses a sliding window on recent call outcomes (success/failure rate over time), the bulkhead uses a sliding window on current concurrency (how many calls are in-flight right now).

Circuit Breaker window: [past 10 seconds of call results]
                        Time-based, backward-looking

Bulkhead window:        [current number of in-flight calls]
                        Instantaneous, forward-limiting

A bulkhead says: "Regardless of how healthy the downstream service looks, I will not let more than N calls be in-flight simultaneously." This prevents:

  • Cascading thread pool exhaustion (one service consuming all available threads)
  • Memory pressure from too many queued requests
  • Back-pressure propagation through the call chain
import java.util.concurrent.*;
 
/**
 * BulkheadCircuitBreaker
 *
 * Combines a sliding window circuit breaker with bulkhead (max concurrent calls).
 * This is exactly what Resilience4j's CircuitBreaker + Bulkhead combination does.
 */
public class BulkheadCircuitBreaker {
 
    private final HystrixStyleCircuitBreaker circuitBreaker;
    private final Semaphore                  bulkhead;
    private final int                        maxConcurrent;
 
    public BulkheadCircuitBreaker(HystrixStyleCircuitBreaker cb, int maxConcurrentCalls) {
        this.circuitBreaker = cb;
        this.maxConcurrent  = maxConcurrentCalls;
        this.bulkhead       = new Semaphore(maxConcurrentCalls, true); // fair ordering
    }
 
    public <T> T execute(HystrixStyleCircuitBreaker.RemoteCall<T> call, T fallback)
            throws InterruptedException {
 
        // Gate 1: Circuit Breaker - is the downstream service healthy?
        if (!circuitBreaker.allowRequest()) {
            System.out.println("[BULKHEAD-CB] Circuit open - fail fast");
            return fallback;
        }
 
        // Gate 2: Bulkhead - is there capacity to make the call?
        // tryAcquire returns immediately (no waiting) - this is the key difference
        // from a blocking semaphore. We want fail-fast, not wait-and-queue.
        if (!bulkhead.tryAcquire()) {
            System.out.printf("[BULKHEAD-CB] At max concurrency (%d) - rejecting%n",
                maxConcurrent);
            // Record as a rejection in the circuit breaker's metrics
            // so the circuit breaker knows the system is under pressure
            circuitBreaker.recordFailure();
            return fallback;
        }
 
        // Both gates passed - make the actual call
        try {
            T result = call.execute();
            circuitBreaker.recordSuccess();
            return result;
        } catch (Exception e) {
            circuitBreaker.recordFailure();
            System.out.println("[BULKHEAD-CB] Call failed: " + e.getMessage());
            return fallback;
        } finally {
            // CRITICAL: always release the semaphore permit, even on exception
            bulkhead.release();
        }
    }
 
    public int availablePermits() { return bulkhead.availablePermits(); }
    public int queueLength()      { return bulkhead.getQueueLength(); }
}

Full Multithreaded Demonstration: All Hazards Exposed and Fixed (Java)

The following demonstration runs 50 concurrent threads against a circuit breaker, shows the race conditions that occur without proper synchronization, then shows the correct behavior with atomics:

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
 
public class MultithreadedCircuitBreakerDemo {
 
    /**
     * BROKEN implementation: demonstrates the race conditions
     * DO NOT USE IN PRODUCTION
     */
    static class BrokenCircuitBreaker {
        private String state = "CLOSED";    // plain String - not thread safe
        private int failureCount = 0;       // plain int - not thread safe
        private long openedAt = 0;
 
        public boolean allowRequest() {
            // RACE: read-check-act is not atomic
            if ("OPEN".equals(state)) {
                if (System.currentTimeMillis() - openedAt > 3000) {
                    state = "HALF_OPEN"; // RACE: multiple threads can do this
                    return true;
                }
                return false;
            }
            return true;
        }
 
        public void recordFailure() {
            failureCount++;  // RACE: not atomic, lost increments
            if (failureCount >= 5) {
                state = "OPEN";  // RACE: multiple threads set this
                openedAt = System.currentTimeMillis();
            }
        }
    }
 
    /**
     * CORRECT implementation: demonstrates all race condition fixes
     */
    static class ThreadSafeCircuitBreaker {
        // AtomicReference for state: CAS-based transitions
        private final AtomicReference<String> state = new AtomicReference<>("CLOSED");
 
        // LongAdder for counters: no contention under any thread count
        private final LongAdder successCount = new LongAdder();
        private final LongAdder failureCount = new LongAdder();
 
        // AtomicLong for openedAt: consistent timestamp
        private final AtomicLong openedAt = new AtomicLong(0);
 
        // AtomicLong for HALF_OPEN probe gate: only ONE thread probes at a time
        private final AtomicLong lastTestedAt = new AtomicLong(0);
 
        private static final int    MIN_REQUESTS       = 10;
        private static final double FAILURE_THRESHOLD  = 0.5;
        private static final long   SLEEP_WINDOW_MS    = 3_000;
 
        public boolean allowRequest() {
            String current = state.get();
            if ("CLOSED".equals(current)) return true;
 
            if ("OPEN".equals(current)) {
                long now     = System.currentTimeMillis();
                long tested  = lastTestedAt.get();
                long opened  = openedAt.get();
 
                if (now > opened + SLEEP_WINDOW_MS) {
                    // CAS: only ONE thread wins and becomes the probe
                    if (lastTestedAt.compareAndSet(tested, now)) {
                        state.compareAndSet("OPEN", "HALF_OPEN");
                        System.out.println("[CB] HALF_OPEN probe allowed to Thread-"
                            + Thread.currentThread().getId());
                        return true;
                    }
                }
                return false; // still within sleep window, or lost CAS probe race
            }
 
            return true; // HALF_OPEN (non-probe threads fall through here)
        }
 
        public void recordSuccess() {
            successCount.increment();
            if ("HALF_OPEN".equals(state.get())) {
                if (state.compareAndSet("HALF_OPEN", "CLOSED")) {
                    System.out.println("[CB] Recovery confirmed -> CLOSED");
                }
            }
        }
 
        public void recordFailure() {
            failureCount.increment();
 
            if ("HALF_OPEN".equals(state.get())) {
                if (state.compareAndSet("HALF_OPEN", "OPEN")) {
                    openedAt.set(System.currentTimeMillis());
                    System.out.println("[CB] Probe failed -> re-OPEN");
                }
                return;
            }
 
            long total    = successCount.sum() + failureCount.sum();
            long failures = failureCount.sum();
 
            if (total >= MIN_REQUESTS && (double) failures / total >= FAILURE_THRESHOLD) {
                // CAS: only the first thread to observe threshold-crossing trips the breaker
                if (state.compareAndSet("CLOSED", "OPEN")) {
                    openedAt.set(System.currentTimeMillis());
                    System.out.printf("[CB] TRIPPED: %d/%d failures (%.0f%%) -> OPEN%n",
                        failures, total, (double) failures / total * 100);
                }
            }
        }
 
        public String getState()    { return state.get(); }
        public long getTotalCalls() { return successCount.sum() + failureCount.sum(); }
        public long getFailures()   { return failureCount.sum(); }
    }
 
    // -------------------------------------------------------------------------
    // Run both implementations under concurrent load and compare
    // -------------------------------------------------------------------------
    public static void main(String[] args) throws InterruptedException {
        System.out.println("==========================================<mark class="obsidian-highlight">");
        System.out.println("Demo: 50 concurrent threads, 30% fail rate");
        System.out.println("</mark>==========================================\n");
 
        int numThreads = 50;
        int callsPerThread = 10;
        ThreadSafeCircuitBreaker cb = new ThreadSafeCircuitBreaker();
        ExecutorService pool = Executors.newFixedThreadPool(numThreads);
        CountDownLatch latch = new CountDownLatch(numThreads);
        AtomicInteger shortCircuited = new AtomicInteger(0);
        AtomicInteger succeeded      = new AtomicInteger(0);
        AtomicInteger failed         = new AtomicInteger(0);
 
        for (int t = 0; t < numThreads; t++) {
            final int threadId = t;
            pool.submit(() -> {
                try {
                    for (int call = 0; call < callsPerThread; call++) {
                        if (!cb.allowRequest()) {
                            shortCircuited.incrementAndGet();
                            continue;
                        }
 
                        // Simulate 30% failure rate
                        boolean isFailure = (threadId * callsPerThread + call) % 10 < 3;
 
                        if (isFailure) {
                            cb.recordFailure();
                            failed.incrementAndGet();
                        } else {
                            cb.recordSuccess();
                            succeeded.incrementAndGet();
                        }
 
                        // Small sleep to create realistic interleaving
                        Thread.sleep(10);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }
 
        latch.await();
        pool.shutdown();
 
        System.out.println("\n=<mark class="obsidian-highlight"> Final Results </mark>=");
        System.out.println("Final state    : " + cb.getState());
        System.out.println("Total calls    : " + cb.getTotalCalls());
        System.out.println("Failures       : " + cb.getFailures());
        System.out.println("Succeeded      : " + succeeded.get());
        System.out.println("Failed         : " + failed.get());
        System.out.println("Short-circuited: " + shortCircuited.get());
    }
}

Full Multithreaded Demonstration (Python)

Python's GIL (Global Interpreter Lock) prevents true CPU-level parallel execution for pure Python code, but I/O-bound threads (like HTTP calls) genuinely run concurrently because the GIL is released during I/O waits. This makes the threading scenarios below realistic for any Python web server (Gunicorn, uWSGI, FastAPI with threading) handling concurrent HTTP requests:

"""
multithreaded_circuit_breaker_demo.py
 
Demonstrates all four circuit breaker concurrency hazards in Python
and shows how to fix each one with threading primitives.
"""
 
import threading
import time
import random
from concurrent.futures import ThreadPoolExecutor, as_completed
from collections import deque
from dataclasses import dataclass
from typing import Optional
 
 
# ===========================================================================<mark class="obsidian-highlight">
# BROKEN implementation - demonstrates the race conditions
# DO NOT USE IN PRODUCTION
# </mark>===========================================================================
 
class BrokenCircuitBreaker:
    """
    Plain Python without any synchronization.
    This will produce incorrect behavior under concurrent access.
    Run the demo to see incorrect counts and duplicate state transitions.
    """
 
    def __init__(self):
        self.state         = "CLOSED"   # not thread-safe
        self.failure_count = 0          # not thread-safe (read-modify-write)
        self.total_count   = 0          # not thread-safe
        self.opened_at     = None
        self.threshold     = 0.5
        self.min_requests  = 10
 
    def allow_request(self) -> bool:
        if self.state == "OPEN":
            if time.time() - self.opened_at > 3:
                self.state = "HALF_OPEN"  # race: multiple threads do this
                return True
            return False
        return True
 
    def record_failure(self):
        self.failure_count += 1   # RACE: += is NOT atomic in a multithreaded context
        self.total_count   += 1   # lost increments possible
        if (self.total_count >= self.min_requests and
                self.failure_count / self.total_count >= self.threshold):
            self.state     = "OPEN"      # race: multiple threads set this
            self.opened_at = time.time()
 
    def record_success(self):
        self.total_count += 1  # RACE
 
 
# ===========================================================================<mark class="obsidian-highlight">
# CORRECT implementation - all hazards fixed
# </mark>===========================================================================
 
class ThreadSafeCircuitBreaker:
    """
    Thread-safe circuit breaker using Python threading primitives:
 
    - threading.Lock for state transitions (replaces Java CAS)
    - threading.Lock per-bucket for counter increments
    - threading.Event for HALF_OPEN single-probe gate
    - Double-checked locking for state transitions
    """
 
    def __init__(self,
                 failure_threshold: float = 0.5,
                 min_requests: int        = 10,
                 sleep_window_seconds: float = 3.0):
        self._failure_threshold = failure_threshold
        self._min_requests      = min_requests
        self._sleep_window      = sleep_window_seconds
 
        self._state      = "CLOSED"
        self._opened_at: Optional[float] = None
 
        # Atomic-style counters using threading.Lock
        # In production use threading.Lock + plain int, or use
        # multiprocessing.Value for true atomics
        self._lock = threading.Lock()
 
        # Sliding window: deque of booleans (True=failure, False=success)
        self._window: deque = deque(maxlen=50)
 
        # HALF_OPEN probe gate: only ONE thread gets to probe
        # threading.Event that is set when a probe is in-flight
        self._probe_in_flight = threading.Event()
 
    def allow_request(self) -> bool:
        with self._lock:
            state = self._state
 
        if state <mark class="obsidian-highlight"> "CLOSED":
            return True
 
        if state </mark> "OPEN":
            opened = self._opened_at
            if opened and (time.time() - opened) >= self._sleep_window:
                # Try to become the sole probe thread
                # threading.Event.set() is idempotent, so we use
                # a compare-and-set pattern via lock
                with self._lock:
                    if self._state == "OPEN":  # double-check under lock
                        self._state = "HALF_OPEN"
                        self._probe_in_flight.clear()  # reset probe gate
                        print(f"[CB] {threading.current_thread().name}: "
                              f"HALF_OPEN - first probe allowed")
                        return True
            return False  # still open
 
        # HALF_OPEN: only allow if no probe is currently in-flight
        if state == "HALF_OPEN":
            # test_and_set: if probe is not in flight, set it and allow this thread
            if not self._probe_in_flight.is_set():
                self._probe_in_flight.set()
                return True
            # Another thread is already probing - reject this one
            return False
 
        return True
 
    def record_success(self):
        with self._lock:
            self._window.append(False)  # False = success
            if self._state == "HALF_OPEN":
                self._state = "CLOSED"
                self._probe_in_flight.clear()
                print(f"[CB] {threading.current_thread().name}: "
                      f"Recovery confirmed -> CLOSED")
 
    def record_failure(self):
        with self._lock:
            self._window.append(True)  # True = failure
 
            if self._state == "HALF_OPEN":
                self._state     = "OPEN"
                self._opened_at = time.time()
                self._probe_in_flight.clear()
                print(f"[CB] {threading.current_thread().name}: "
                      f"Probe failed -> re-OPEN")
                return
 
            if self._state == "OPEN":
                return
 
            # Evaluate threshold from CLOSED
            if len(self._window) >= self._min_requests:
                failure_rate = sum(self._window) / len(self._window)
                if failure_rate >= self._failure_threshold:
                    self._state     = "OPEN"
                    self._opened_at = time.time()
                    print(f"[CB] {threading.current_thread().name}: "
                          f"Tripped! {failure_rate:.0%} failure rate -> OPEN")
 
    @property
    def state(self) -> str:
        with self._lock:
            return self._state
 
    def metrics(self) -> dict:
        with self._lock:
            total    = len(self._window)
            failures = sum(self._window)
            return {
                "state":        self._state,
                "total_calls":  total,
                "failures":     failures,
                "failure_rate": f"{failures/total*100:.1f}%" if total > 0 else "0%"
            }
 
 
# ===========================================================================<mark class="obsidian-highlight">
# Concurrent load simulation
# </mark>===========================================================================
 
def simulate_concurrent_load(cb: ThreadSafeCircuitBreaker,
                              num_threads: int = 50,
                              failure_rate: float = 0.4):
    """
    Simulates a realistic web server scenario:
    - num_threads concurrent requests arrive simultaneously
    - failure_rate of calls to downstream service fail
    - Some calls are slow (simulating timeout scenario)
    """
    results = {"allowed": 0, "rejected": 0, "succeeded": 0, "failed": 0}
    results_lock = threading.Lock()
 
    def make_request(thread_id: int):
        allowed = cb.allow_request()
        with results_lock:
            if not allowed:
                results["rejected"] += 1
                return
            results["allowed"] += 1
 
        # Simulate network I/O (GIL is released here, true parallelism)
        time.sleep(random.uniform(0.001, 0.01))
 
        # Simulate downstream service behavior
        if random.random() < failure_rate:
            cb.record_failure()
            with results_lock:
                results["failed"] += 1
        else:
            cb.record_success()
            with results_lock:
                results["succeeded"] += 1
 
    with ThreadPoolExecutor(max_workers=num_threads,
                            thread_name_prefix="Worker") as executor:
        futures = [executor.submit(make_request, i) for i in range(num_threads)]
        for f in as_completed(futures):
            f.result()  # re-raise any exceptions
 
    return results
 
 
def run_full_demo():
    print("=" * 65)
    print("Multithreaded Circuit Breaker Demo")
    print("=" * 65)
 
    cb = ThreadSafeCircuitBreaker(
        failure_threshold=0.5,
        min_requests=10,
        sleep_window_seconds=3.0
    )
 
    print("\n--- Phase 1: Low failure rate (20%) - circuit should stay CLOSED ---")
    results = simulate_concurrent_load(cb, num_threads=30, failure_rate=0.2)
    print(f"Allowed: {results['allowed']}  "
          f"Rejected: {results['rejected']}  "
          f"Succeeded: {results['succeeded']}  "
          f"Failed: {results['failed']}")
    print(f"Circuit: {cb.metrics()}\n")
 
    print("--- Phase 2: High failure rate (70%) - circuit should OPEN ---")
    results = simulate_concurrent_load(cb, num_threads=30, failure_rate=0.7)
    print(f"Allowed: {results['allowed']}  "
          f"Rejected: {results['rejected']}  "
          f"Succeeded: {results['succeeded']}  "
          f"Failed: {results['failed']}")
    print(f"Circuit: {cb.metrics()}\n")
 
    print("--- Phase 3: Calls during OPEN state - should be rejected ---")
    results = simulate_concurrent_load(cb, num_threads=20, failure_rate=0.0)
    print(f"Allowed: {results['allowed']}  "
          f"Rejected: {results['rejected']}")
    print(f"Circuit: {cb.metrics()}\n")
 
    print(f"--- Phase 4: Wait {3.1:.1f}s for sleep window, then probe (HALF_OPEN) ---")
    time.sleep(3.1)
 
    print("--- Phase 5: Service recovered - probe should succeed ---")
    results = simulate_concurrent_load(cb, num_threads=20, failure_rate=0.0)
    print(f"Allowed: {results['allowed']}  "
          f"Rejected: {results['rejected']}  "
          f"Succeeded: {results['succeeded']}")
    print(f"Final circuit state: {cb.metrics()}")
 
 
if __name__ == "__main__":
    run_full_demo()

Concurrency Hazard Summary Table

HazardConsequenceHystrix FixResilience4j FixPython Fix
Check-then-act on allowRequestState transition is not atomic; multiple threads trip the circuitAtomicReference.compareAndSet on stateAtomicReference + synchronizedthreading.Lock with double-check
HALF_OPEN thundering herdHundreds of probe requests overwhelm recovering serviceCAS on lastTestedAt timestampAtomicInteger permit counterthreading.Event as probe gate
Bucket counter write-write conflictLost increments; failure rate is understated; circuit does not open when it shouldLongAdder per counterAtomicInteger accumulatorthreading.Lock per bucket
Bucket rollover duplicate creationMultiple Bucket objects at same slot; writes split across them; counts wrongsynchronized rollover block + AtomicReference.set on current bucketsynchronized writethreading.Lock with double-check

11.3 Resilience4j - The Modern Evolution and How It Differs From Hystrix

Resilience4j was written as a replacement for Hystrix (which Netflix put into maintenance mode in 2018). It is lighter, modular, and offers two genuinely different window types — addressing the limitations of Hystrix's bucket-only approach.

11.3.1 COUNT_BASED Sliding Window

This is a true per-call sliding window using a circular array (ring buffer) of fixed size.

Configuration: slidingWindowSize = 5, slidingWindowType = COUNT_BASED

Internal data structure: int[] ringBuffer = new int[5] (stores SUCCESS=1 or FAILURE=0)
                         int head = 0  (points to the oldest entry, about to be overwritten)

State after 5 calls: [1, 1, 0, 0, 0]  -> 3 failures out of 5 = 60%
                      S  S  F  F  F

Call 6 arrives (SUCCESS):
  - Overwrite position head=0 (oldest entry, previously SUCCESS):
    [1, 1, 0, 0, 0] -> position 0 was SUCCESS, new entry is SUCCESS
    net change: +1 success -1 success = no change
  head = 1

  New window: [1, 1, 0, 0, 0]  -> still 3/5 = 60%
              ^new   S  F  F  F

Call 7 arrives (SUCCESS):
  - Overwrite position head=1 (oldest, previously SUCCESS):
    net change: success replaces success = no change
  head = 2

  New window: [1, 1, 0, 0, 0] -> still 3/5 = 60%

Call 8 arrives (SUCCESS):
  - Overwrite position head=2 (oldest, previously FAILURE):
    net change: +1 success, -1 failure
  head = 3

  New window: [1, 1, 1, 0, 0] -> 2/5 = 40%  -- below threshold if threshold = 50%

The ring buffer means memory is O(window_size) regardless of time. Writes and reads are O(1). This is a perfect fixed-size sliding window.

// Resilience4j COUNT_BASED configuration (Spring Boot)
CircuitBreakerConfig countBasedConfig = CircuitBreakerConfig.custom()
    .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
    .slidingWindowSize(10)               // last 10 calls
    .minimumNumberOfCalls(5)             // minimum calls before evaluation
    .failureRateThreshold(50.0f)         // open if >= 50% failures
    .waitDurationInOpenState(Duration.ofSeconds(30))
    .permittedNumberOfCallsInHalfOpenState(3)  // probe with 3 calls in HALF_OPEN
    .build();

11.3.2 TIME_BASED Sliding Window

This is a per-epoch-second bucket window similar to Hystrix but with finer granularity: one bucket per second (fixed, not configurable).

Configuration: slidingWindowSize = 10, slidingWindowType = TIME_BASED
               -> last 10 seconds, one bucket per second = 10 buckets

Each bucket stores: total calls, failed calls, slow calls (duration > slowCallThreshold)

The window slides every second automatically.
// Resilience4j TIME_BASED configuration
CircuitBreakerConfig timeBasedConfig = CircuitBreakerConfig.custom()
    .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
    .slidingWindowSize(10)                         // last 10 seconds
    .minimumNumberOfCalls(20)                      // minimum calls in window
    .failureRateThreshold(50.0f)
    .slowCallRateThreshold(100.0f)                 // UNIQUE to Resilience4j
    .slowCallDurationThreshold(Duration.ofSeconds(2)) // calls >2s count as "slow"
    .waitDurationInOpenState(Duration.ofSeconds(60))
    .build();

The slowCallRateThreshold feature: This is a capability Hystrix did not have. Resilience4j can open the circuit not just on outright failures, but on calls that succeed but take too long. A service that responds in 10 seconds instead of 100ms is effectively useless — it might as well have failed. Resilience4j handles this explicitly with the slow call rate threshold.

11.3.3 Side-by-Side Comparison: Hystrix vs. Resilience4j COUNT_BASED vs. TIME_BASED

DimensionHystrix Rolling WindowResilience4j COUNT_BASEDResilience4j TIME_BASED
Window typeTime-based (bucket-based)Count-based (ring buffer)Time-based (per-second buckets)
Window unitMillisecondsNumber of callsSeconds
Default window10 secondsConfigurableConfigurable
Bucket granularityConfigurable (default 1s)N/A - per callFixed: 1 second
MemoryO(num_buckets)O(window_size)O(window_size in seconds)
AccuracySub-bucket impreciseExact per-callSub-second imprecise
Slow call detectionNo (only failures/timeouts)NoYes (slowCallRateThreshold)
Minimum requests gateYes (requestVolumeThreshold)Yes (minimumNumberOfCalls)Yes (minimumNumberOfCalls)
Thread safety mechanismAtomic LongAdder per bucketSynchronized ring bufferAtomic LongAdder per bucket
Library statusMaintenance mode (2018)Actively maintainedActively maintained
Java 8+ functional APINoYesYes

11.3.4 The Internal Data Structure for Each Window Type - Visualized

HYSTRIX - Rolling Number (bucket array):

  buckets[] = [ B0 | B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 | B9 ]
                ^                                              ^
              oldest                                        newest
              (evicted                                  (current second)
               on next
               rollover)

  Each bucket: { AtomicLong success, failure, timeout, rejected, shortCircuited }
  currentBucketIndex = (System.currentTimeMillis() / bucketSizeMs) % numBuckets


RESILIENCE4J COUNT_BASED - Ring Buffer:

  ringBuffer[] = [ R0 | R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 ]
                          ^
                        headIndex (next write position, overwrites oldest)

  failureCount and totalCount maintained as running totals.
  On each write: add new, subtract evicted (the one being overwritten).
  O(1) read and write.


RESILIENCE4J TIME_BASED - Epoch Second Buckets:

  Map<Long epochSecond, Bucket> buckets  (only last N seconds kept)
  Current second = System.currentTimeMillis() / 1000

  On each call: write to bucket[currentSecond]
  On each evaluation: sum all buckets where key >= (currentSecond - windowSize)
  Eviction: buckets older than windowSize seconds are discarded.

11.3.5 When to Use Each Window Type

Use COUNT_BASED when:

  • Traffic is steady and predictable.
  • You care about exact per-call accuracy.
  • Traffic can occasionally drop to near zero (time-based windows suffer during low traffic - a window might have very few calls, making the failure rate meaningless).
  • Example: internal service calls where traffic is always flowing.

Use TIME_BASED when:

  • Traffic is bursty (many calls in a short burst, then silence).
  • You want the window to represent a real-world time period (e.g., "open if more than 50% of requests in the last 60 seconds failed").
  • You need slow call detection alongside failure detection.
  • Example: external API calls where load varies dramatically.

Use Hystrix (legacy) when:

  • You are maintaining an existing Hystrix-based system.
  • Do not start new projects with Hystrix; migrate to Resilience4j.

11.3.6 A Concrete Illustration of Count vs. Time Window Behavior Difference

Scenario: 10 calls happen in burst at t=0, then silence for 9 seconds.
          Window size = 10.   Failure threshold = 50%.

COUNT_BASED (window = last 10 calls):
  After the burst:  window = [F,F,F,F,F,S,S,S,S,S]  -> 5/10 = 50% -> threshold met
  At t=5s:          window is STILL [F,F,F,F,F,S,S,S,S,S] (no new calls to slide it)
  -> COUNT_BASED window does not move until new calls arrive.
  -> Stale failure data stays in the window indefinitely.

TIME_BASED (window = last 10 seconds):
  After the burst:  window covers t=0 to t=10 -> 5/10 = 50%
  At t=5s:          early failures from the burst are still in window
  At t=10s:         all 10 burst calls slide out of the 10-second window
  -> window is now EMPTY -> circuit CLOSES (or stays CLOSED - no data to evaluate)
  -> Failure data automatically expires even with zero new calls.

This is the fundamental behavioral difference: count-based windows remember failures until they are pushed out by new calls; time-based windows automatically forget old failures after the time window elapses.


11.4 The Two Sliding Window Modes in Resilience4j (Summary Reference)

Modern circuit breakers like Resilience4j offer two window types:

Count-Based Sliding Window:

Window size = 5 (last 5 calls)

Call history: [SUCCESS, SUCCESS, FAILURE, FAILURE, FAILURE]
                                                  ^--- newest
Failure rate = 3/5 = 60%
If threshold = 50%, circuit opens.

Next call:    [SUCCESS, FAILURE, FAILURE, FAILURE, NEW_CALL]
              ^--- oldest call evicted when new call enters

This is exactly a fixed-size sliding window over the call result history.

Time-Based Sliding Window:

Window = last 60 seconds

t=0:  [call1, call2, call3, ...]
t=30: [call1, call2, ..., call_n]
t=60: [call2, ..., call_n, new_calls]  (call1 evicted - older than 60s)

This is a time-based sliding window where the window boundary slides with the current time.

11.5 Implementation of Circuit Breaker Sliding Window (Java, Simplified)

import java.util.*;
import java.util.concurrent.atomic.*;
 
public class SimpleCircuitBreaker {
 
    enum State { CLOSED, OPEN, HALF_OPEN }
    enum CallResult { SUCCESS, FAILURE }
 
    private final int windowSize;
    private final double failureThreshold; // e.g., 0.5 = 50%
    private final long openDurationMs;
 
    private State state = State.CLOSED;
    private final Deque<CallResult> window = new ArrayDeque<>();
    private int failureCount = 0;
    private long openedAt = 0;
 
    public SimpleCircuitBreaker(int windowSize, double failureThreshold, long openDurationMs) {
        this.windowSize = windowSize;
        this.failureThreshold = failureThreshold;
        this.openDurationMs = openDurationMs;
    }
 
    public synchronized boolean allowRequest() {
        if (state == State.OPEN) {
            if (System.currentTimeMillis() - openedAt >= openDurationMs) {
                state = State.HALF_OPEN;
                return true; // allow one probe request
            }
            return false; // still open, reject
        }
        return true; // CLOSED or HALF_OPEN
    }
 
    public synchronized void recordResult(CallResult result) {
        if (state <mark class="obsidian-highlight"> State.OPEN) return;
 
        // Sliding window: add new result
        window.addLast(result);
        if (result </mark> CallResult.FAILURE) failureCount++;
 
        // Evict oldest result if window is full
        if (window.size() > windowSize) {
            CallResult evicted = window.pollFirst();
            if (evicted <mark class="obsidian-highlight"> CallResult.FAILURE) failureCount--;
        }
 
        // Only evaluate once we have a full window
        if (window.size() </mark> windowSize) {
            double failureRate = (double) failureCount / windowSize;
 
            if (failureRate >= failureThreshold) {
                state = State.OPEN;
                openedAt = System.currentTimeMillis();
            } else if (state == State.HALF_OPEN) {
                // Recovery confirmed
                state = State.CLOSED;
                window.clear();
                failureCount = 0;
            }
        }
    }
}

11.6 Other Resilience Patterns Using Sliding Windows

Bulkhead Pattern: Tracks concurrent requests per service in a sliding window to enforce isolation.

Retry with Exponential Backoff: The number of retries within the last N attempts is tracked in a sliding window to prevent retry storms.

Timeout Detection: P99 timeout rates computed over a sliding time window to detect latency degradation.


11.7 The Website Crawler Case Study - Why This Is Exactly Where Circuit Breaker Belongs

Common misconception: "A standalone website crawler that hits URLs independently does not need a circuit breaker."

Reality: This is one of the most compelling, high-value use cases for the circuit breaker pattern. The absence of it is precisely what turns a crawler into a catastrophic time-wasting process.

The Problem Without a Circuit Breaker

Imagine a crawler with 10,000 URLs to process. The target website goes down at URL #50.

Here is what happens with naive retry-on-timeout logic:

URL #50  -> connect attempt -> wait 30s -> timeout -> retry 1 -> wait 30s
         -> timeout -> retry 2 -> wait 30s -> timeout -> retry 3 -> FAIL
         Total time wasted: 4 * 30s = 120 seconds for ONE URL

URL #51  -> same story -> 120 seconds wasted
URL #52  -> same story -> 120 seconds wasted
...
URL #10,000 -> same story -> 120 seconds wasted

Total time wasted: 9,950 URLs * 120 seconds = 1,194,000 seconds = ~13.8 DAYS

The crawler runs for nearly 14 days and produces zero useful results for all those URLs, because the site was down. All that CPU time, memory, network connections, and log noise were 100% waste.

Why This Specific Scenario Fits the Circuit Breaker Perfectly

The circuit breaker pattern was designed for exactly this failure mode. Let us break down why each characteristic of the crawler maps perfectly to circuit breaker's purpose:

Crawler CharacteristicWhy Circuit Breaker Applies
Many requests to the same hostFailures are correlated - if the host is down, ALL requests to it will fail
Expensive per-request timeout30 seconds per failure * thousands of URLs = unacceptable total cost
Retry mechanism already in placeRetries amplify the problem exponentially; circuit breaker gates retries
No human in the loopAutomated system needs automated self-protection
Goal is data collectionNo data is collected during outage; failing fast and moving on is better

The key insight is that failures are not independent. In a distributed microservices call, if Service B is down, every call from Service A to Service B will fail. In a crawler, if www.example.com is down, every URL under www.example.com will fail. The failures are 100% correlated by host. A circuit breaker at the host level collapses all of those individual failures into one coordinated fast-fail.

What the Crawl Looks Like WITH a Circuit Breaker

Per-host circuit breaker, window = 5 calls, failure threshold = 60%, open duration = 5 minutes

URL #50  -> FAIL (timeout 30s)   | window: [F]        failure rate: 1/1
URL #51  -> FAIL (timeout 30s)   | window: [F,F]      failure rate: 2/2
URL #52  -> FAIL (timeout 30s)   | window: [F,F,F]    failure rate: 3/3
URL #53  -> FAIL (timeout 30s)   | window: [F,F,F,F]  failure rate: 4/4
URL #54  -> FAIL (timeout 30s)   | window: [F,F,F,F,F] failure rate: 5/5 = 100%

CIRCUIT OPENS for www.example.com

URL #55  -> REJECTED INSTANTLY (circuit open) -> log and skip
URL #56  -> REJECTED INSTANTLY -> log and skip
...
URL #10,000 -> REJECTED INSTANTLY -> log and skip

After 5 minutes:
Circuit enters HALF-OPEN -> probe URL sent -> still failing -> back to OPEN
After another 5 minutes:
Circuit enters HALF-OPEN -> probe URL succeeds -> CLOSED -> crawling resumes

Time cost comparison:

Without circuit breaker:
  5 slow failures (5 * 120s)    =   600 seconds
  9,945 slow failures           = 1,193,400 seconds
  Total                         = 1,194,000 seconds  (~13.8 days)

With circuit breaker:
  5 slow failures (5 * 120s)    =   600 seconds
  9,945 instant rejections      =   ~0 seconds
  Total                         =   600 seconds  (~10 minutes)

Speedup: 1994x faster

The crawler skips the dead host entirely, continues processing all other URLs productively, and comes back to probe the dead host after the configured recovery window.

The Right Architecture: Per-Host Circuit Breaker

The circuit breaker must be scoped per host (domain), not per URL. A single circuit breaker for the entire crawler would be wrong - one dead host should not block crawling of all other hosts.

import java.util.*;
import java.util.concurrent.*;
 
public class CircuitBreakerCrawler {
 
    // One circuit breaker per unique host
    private final Map<String, SimpleCircuitBreaker> breakers = new ConcurrentHashMap<>();
 
    // Configuration
    private static final int WINDOW_SIZE = 5;
    private static final double FAILURE_THRESHOLD = 0.6; // 60%
    private static final long OPEN_DURATION_MS = 5 * 60 * 1000L; // 5 minutes
    private static final int CONNECT_TIMEOUT_MS = 10_000; // 10 seconds (not 30)
    private static final int MAX_RETRIES = 1; // retries are pointless on a dead host
 
    public CrawlResult crawl(String url) {
        String host = extractHost(url);
 
        // Get or create the circuit breaker for this host
        SimpleCircuitBreaker breaker = breakers.computeIfAbsent(
            host,
            h -> new SimpleCircuitBreaker(WINDOW_SIZE, FAILURE_THRESHOLD, OPEN_DURATION_MS)
        );
 
        // Check if the circuit breaker allows this request
        if (!breaker.allowRequest()) {
            // Fail fast - do not even attempt the connection
            return CrawlResult.circuitOpen(url, host);
        }
 
        // Attempt the actual HTTP request
        try {
            String content = httpGet(url, CONNECT_TIMEOUT_MS);
            breaker.recordResult(SimpleCircuitBreaker.CallResult.SUCCESS);
            return CrawlResult.success(url, content);
        } catch (Exception e) {
            breaker.recordResult(SimpleCircuitBreaker.CallResult.FAILURE);
            return CrawlResult.failure(url, e.getMessage());
        }
    }
 
    public void crawlAll(List<String> urls) {
        int skipped = 0;
        int succeeded = 0;
        int failed = 0;
 
        for (String url : urls) {
            CrawlResult result = crawl(url);
 
            switch (result.status) {
                case SUCCESS:
                    succeeded++;
                    processContent(result);
                    break;
                case CIRCUIT_OPEN:
                    skipped++;
                    logSkipped(result); // log for later retry
                    break;
                case FAILURE:
                    failed++;
                    logFailure(result);
                    break;
            }
        }
 
        System.out.printf("Done. Success: %d, Failed: %d, Skipped (circuit open): %d%n",
            succeeded, failed, skipped);
    }
 
    private String extractHost(String url) {
        // Extract "www.example.com" from "https://www.example.com/path"
        try {
            return new java.net.URL(url).getHost();
        } catch (Exception e) {
            return url;
        }
    }
 
    private String httpGet(String url, int timeoutMs) throws Exception {
        // Actual HTTP implementation omitted for brevity
        // In production: use Apache HttpClient, OkHttp, or java.net.http.HttpClient
        // with explicit connection and read timeout configuration
        throw new UnsupportedOperationException("Implement with your HTTP client");
    }
 
    private void processContent(CrawlResult result) { /* index, store, parse */ }
    private void logSkipped(CrawlResult result) { /* write to retry queue */ }
    private void logFailure(CrawlResult result) { /* write to error log */ }
 
 
    // -------------------------------------------------------------------------
    // Supporting types
    // -------------------------------------------------------------------------
 
    enum CrawlStatus { SUCCESS, FAILURE, CIRCUIT_OPEN }
 
    static class CrawlResult {
        final String url;
        final String host;
        final CrawlStatus status;
        final String content;
        final String errorMessage;
 
        private CrawlResult(String url, String host, CrawlStatus status,
                            String content, String errorMessage) {
            this.url = url;
            this.host = host;
            this.status = status;
            this.content = content;
            this.errorMessage = errorMessage;
        }
 
        static CrawlResult success(String url, String content) {
            return new CrawlResult(url, null, CrawlStatus.SUCCESS, content, null);
        }
 
        static CrawlResult failure(String url, String error) {
            return new CrawlResult(url, null, CrawlStatus.FAILURE, null, error);
        }
 
        static CrawlResult circuitOpen(String url, String host) {
            return new CrawlResult(url, host, CrawlStatus.CIRCUIT_OPEN, null,
                "Circuit open for host: " + host);
        }
    }
}

Python equivalent:

from collections import defaultdict, deque
from urllib.parse import urlparse
import time
from dataclasses import dataclass
from enum import Enum
from typing import Optional
import requests
 
 
class CrawlStatus(Enum):
    SUCCESS = "SUCCESS"
    FAILURE = "FAILURE"
    CIRCUIT_OPEN = "CIRCUIT_OPEN"
 
 
@dataclass
class CrawlResult:
    url: str
    status: CrawlStatus
    content: Optional[str] = None
    error: Optional[str] = None
    host: Optional[str] = None
 
 
class HostCircuitBreaker:
    """Sliding window circuit breaker scoped to a single host."""
 
    def __init__(self, window_size: int = 5, failure_threshold: float = 0.6,
                 open_duration_seconds: float = 300.0):
        self.window_size = window_size
        self.failure_threshold = failure_threshold
        self.open_duration_seconds = open_duration_seconds
 
        self._window: deque = deque()
        self._failure_count = 0
        self._state = "CLOSED"
        self._opened_at: Optional[float] = None
 
    @property
    def state(self) -> str:
        return self._state
 
    def allow_request(self) -> bool:
        if self._state == "OPEN":
            if time.time() - self._opened_at >= self.open_duration_seconds:
                self._state = "HALF_OPEN"
                return True
            return False
        return True
 
    def record_success(self):
        self._record(is_failure=False)
        if self._state == "HALF_OPEN":
            self._state = "CLOSED"
            self._window.clear()
            self._failure_count = 0
 
    def record_failure(self):
        self._record(is_failure=True)
 
    def _record(self, is_failure: bool):
        if self._state == "OPEN":
            return
 
        # Sliding window add
        self._window.append(is_failure)
        if is_failure:
            self._failure_count += 1
 
        # Evict oldest if over capacity
        if len(self._window) > self.window_size:
            evicted = self._window.popleft()
            if evicted:
                self._failure_count -= 1
 
        # Evaluate threshold only on full window
        if len(self._window) == self.window_size:
            rate = self._failure_count / self.window_size
            if rate >= self.failure_threshold:
                self._state = "OPEN"
                self._opened_at = time.time()
                print(f"[CIRCUIT OPEN] failure rate {rate:.0%} exceeded threshold")
 
 
class CircuitBreakerCrawler:
    """
    Website crawler with per-host sliding window circuit breakers.
    Fails fast on known-dead hosts instead of waiting for each timeout.
    """
 
    def __init__(self,
                 window_size: int = 5,
                 failure_threshold: float = 0.6,
                 open_duration_seconds: float = 300.0,
                 connect_timeout_seconds: float = 10.0):
        self._breakers: dict[str, HostCircuitBreaker] = {}
        self._window_size = window_size
        self._failure_threshold = failure_threshold
        self._open_duration = open_duration_seconds
        self._timeout = connect_timeout_seconds
 
    def _get_breaker(self, host: str) -> HostCircuitBreaker:
        if host not in self._breakers:
            self._breakers[host] = HostCircuitBreaker(
                self._window_size,
                self._failure_threshold,
                self._open_duration
            )
        return self._breakers[host]
 
    def crawl(self, url: str) -> CrawlResult:
        host = urlparse(url).netloc
        breaker = self._get_breaker(host)
 
        if not breaker.allow_request():
            # Instant rejection - no network call made, no timeout waited
            return CrawlResult(url=url, status=CrawlStatus.CIRCUIT_OPEN,
                               host=host, error=f"Circuit OPEN for {host}")
 
        try:
            response = requests.get(url, timeout=self._timeout)
            response.raise_for_status()
            breaker.record_success()
            return CrawlResult(url=url, status=CrawlStatus.SUCCESS,
                               content=response.text)
        except Exception as e:
            breaker.record_failure()
            return CrawlResult(url=url, status=CrawlStatus.FAILURE,
                               error=str(e))
 
    def crawl_all(self, urls: list[str]) -> dict:
        results = {s: [] for s in CrawlStatus}
 
        for url in urls:
            result = self.crawl(url)
            results[result.status].append(result)
 
            if result.status <mark class="obsidian-highlight"> CrawlStatus.CIRCUIT_OPEN:
                print(f"[SKIP] {url} - circuit open for host {result.host}")
            elif result.status </mark> CrawlStatus.FAILURE:
                print(f"[FAIL] {url} - {result.error}")
            else:
                print(f"[OK]   {url} - {len(result.content)} bytes")
 
        print(f"\nSummary:")
        print(f"  Succeeded  : {len(results[CrawlStatus.SUCCESS])}")
        print(f"  Failed     : {len(results[CrawlStatus.FAILURE])}")
        print(f"  Skipped    : {len(results[CrawlStatus.CIRCUIT_OPEN])} (circuit open - instant)")
 
        return results

Additional Crawler-Specific Enhancements Worth Knowing

1. Per-host timeout reduction when circuit is HALF-OPEN

When probing a recovering host (HALF-OPEN state), use a much shorter timeout. If the site is genuinely back, it will respond quickly. If it is still flaky, you find out fast.

2. Robots.txt and rate limit circuit breaker

A 429 (Too Many Requests) response is a specific signal that should open the circuit for a shorter duration (e.g., respect Retry-After headers) rather than triggering the full failure circuit.

3. Sliding window per error type

Separate circuit breakers for:

  • Connection refused / host unreachable (infrastructure down)
  • HTTP 5xx errors (application down)
  • HTTP 4xx errors (should NOT trip the circuit - these are valid responses)
  • Timeouts (network degradation)

Only connection failures and 5xx errors should count as failures in the circuit breaker window. A 404 means the URL does not exist, which is valuable information, not a failure.

4. Skipped URLs go to a retry queue

URLs that were skipped because the circuit was open should be pushed into a persistent queue (a file, a Redis list, a database table) for re-crawling after the circuit closes - not silently dropped.

# Pseudocode for retry queue integration
def crawl_all_with_retry_queue(urls: list[str], retry_queue_file: str):
    crawler = CircuitBreakerCrawler()
    skipped_for_retry = []
 
    for url in urls:
        result = crawler.crawl(url)
        if result.status == CrawlStatus.CIRCUIT_OPEN:
            skipped_for_retry.append(url)
 
    # Persist skipped URLs for a future crawl run
    with open(retry_queue_file, 'a') as f:
        for url in skipped_for_retry:
            f.write(url + '\n')
 
    print(f"{len(skipped_for_retry)} URLs written to retry queue: {retry_queue_file}")

Summary: The Decision Matrix

ScenarioIs Circuit Breaker Appropriate?Why
Crawler hitting 1 URL on a dead hostMarginal benefitOnly 1 URL; no cascade to prevent
Crawler hitting 100+ URLs on the same dead hostHighly beneficialPrevents 100+ * timeout_seconds of waste
Crawler hitting diverse hosts, one is downEssentialPrevents one dead host from starving all others
Crawler in a time-sensitive job (CI pipeline, scheduled task)CriticalTimeout storms can blow job time budget
Crawler with no retry logic at allReduced benefitStill useful for fast-fail on subsequent runs
Crawler with 3x retries per URLMaximum benefitRetries multiply the timeout cost; circuit breaker eliminates all of them

The rule of thumb: If your caller will make more than one request to the same destination, and that destination can fail, a circuit breaker is worth the implementation cost. A crawler with thousands of URLs from the same domain is the textbook definition of this scenario.


12. Networking and Protocol Engineering

12.1 TCP Sliding Window Protocol

The TCP (Transmission Control Protocol) sliding window is one of the most fundamental applications in all of networking. Every HTTP request you make, every file you download, every video you stream uses this mechanism.

The Problem TCP Solves:

  • The sender wants to send data as fast as possible.
  • The receiver has a limited buffer.
  • The network has limited bandwidth and introduces packet loss and reordering.

Without a sliding window, TCP would use stop-and-wait: send one packet, wait for acknowledgment (ACK), then send the next. With a 100ms round-trip time (RTT) and 1500-byte packets, this yields only 12,000 bytes/second = 12 KB/s on a gigabit network. Catastrophically inefficient.

The TCP Sliding Window Solution:

Sender:
                    Window Size = 4 packets
                    |___________________|
Packet: [1][2][3][4][5][6][7][8][9][10]
         ^                   ^
         |                   |
     already sent         not yet sent
     and ACKed

After ACK for packet 1:
                         |___________________|
Packet: [1][2][3][4][5][6][7][8][9][10]
                          ^               ^
                    window slides forward

The sender can have up to window_size packets "in flight" (sent but not yet acknowledged). As ACKs arrive, the window slides forward, allowing more packets to be sent. This keeps the network pipe full.

Flow Control: The receiver advertises its available buffer size as the window size in each ACK packet. This prevents the sender from overwhelming the receiver.

Congestion Control: TCP adjusts the sender's window size based on detected packet loss (indicating network congestion), implementing algorithms like Slow Start, AIMD (Additive Increase Multiplicative Decrease), CUBIC, and BBR.

12.2 HTTP/2 and HTTP/3 Flow Control

HTTP/2 and HTTP/3 implement per-stream and per-connection sliding window flow control at the application layer, independent of TCP. Each stream has a flow control window, and DATA frames can only be sent if the window allows it. WINDOW_UPDATE frames slide the window forward.

12.3 QUIC Protocol

QUIC (the protocol underlying HTTP/3) implements its own sliding window for both flow control and reliability, independent of the OS TCP stack.

12.4 Network Intrusion Detection Systems (IDS)

Systems like Snort and Suricata use sliding windows over packet timestamps to detect attack patterns:

  • Port scanning: more than N unique destination ports within a W-second window
  • DDoS detection: more than N packets/bytes from a source within a W-second window
  • Brute-force detection: more than N failed authentication attempts within a W-second window

13. Databases, Analytics, and Streaming Systems

13.1 SQL Window Functions

SQL window functions are directly named after the sliding window concept. They compute a value for each row based on a "window" of related rows.

-- Moving average of sales over the last 7 days
SELECT
    sale_date,
    amount,
    AVG(amount) OVER (
        ORDER BY sale_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS moving_avg_7_day
FROM daily_sales;
 
-- Running total with a sliding 30-day window
SELECT
    event_date,
    revenue,
    SUM(revenue) OVER (
        ORDER BY event_date
        RANGE BETWEEN INTERVAL '29' DAY PRECEDING AND CURRENT ROW
    ) AS rolling_30_day_revenue
FROM events;

The ROWS BETWEEN and RANGE BETWEEN clauses define the window boundaries. As the query processes each row, the window slides forward.

Window function categories:

  • Aggregate functions: SUM, AVG, COUNT, MIN, MAX over the window
  • Ranking functions: ROW_NUMBER, RANK, DENSE_RANK, NTILE
  • Navigation functions: LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE

Stream processing systems process unbounded data in real-time using window operations as a first-class primitive.

Kafka Streams window types:

Tumbling Window (non-overlapping, fixed-size):
|--W1--|--W2--|--W3--|--W4--|
  time ->

Hopping Window (overlapping, fixed-size, advances by hop):
|--W1--|
   |--W2--|
      |--W3--|

Sliding Window (continuous, triggered by new events):
Advances by one event at a time. True sliding window.

Session Window (variable-size, bounded by inactivity gap):
|--session1--|   |--session2--|
              gap (inactivity)

Apache Flink sliding window example (conceptual):

// Flink DataStream API
dataStream
    .keyBy(event -> event.userId)
    .window(SlidingEventTimeWindows.of(
        Time.minutes(10),    // window size
        Time.minutes(1)      // slide interval
    ))
    .aggregate(new ClickCountAggregator());

13.3 Time-Series Databases (InfluxDB, TimescaleDB)

Time-series databases are built around sliding window queries as their primary operation:

  • Anomaly detection over the last N data points
  • Downsampling: compute average of raw data in windows to create lower-resolution views
  • Capacity planning: rolling 95th percentile of resource utilization

13.4 Redis Sorted Sets as Sliding Window Stores

Redis sorted sets (ZSET) are the standard production implementation for sliding window rate limiters:

ZADD user:123:requests <current_timestamp> <unique_request_id>
ZREMRANGEBYSCORE user:123:requests 0 <current_timestamp - window_size>
ZCARD user:123:requests   -> current count in window

Elements are sorted by timestamp. Eviction of old elements is O(log n). Count query is O(1). This is a time-based sliding window with O(log n) insertion and eviction.


14. Machine Learning and Signal Processing

14.1 Moving Averages in Financial Analysis

The most visible use of sliding windows in finance:

  • Simple Moving Average (SMA): Unweighted average of the last N periods.
  • Exponential Moving Average (EMA): Weighted average where more recent data has exponentially higher weight.
  • Bollinger Bands: Mean and standard deviation computed over a sliding window of 20 periods, with bands at 2 standard deviations.

These are used to smooth price data, identify trends, and generate trading signals.

def simple_moving_average(prices: list[float], n: int) -> list[float]:
    """Classic fixed-size sliding window for financial SMA."""
    result = []
    window_sum = sum(prices[:n])
    result.append(window_sum / n)
 
    for i in range(n, len(prices)):
        window_sum += prices[i]
        window_sum -= prices[i - n]
        result.append(window_sum / n)
 
    return result

14.2 Feature Engineering for Time-Series Machine Learning

When building ML models on time-series data (fraud detection, predictive maintenance, demand forecasting), sliding window feature engineering is standard:

import pandas as pd
 
def create_sliding_window_features(df: pd.DataFrame, target_col: str, windows: list) -> pd.DataFrame:
    """
    For each window size, compute rolling statistics as ML features.
    Each row's features are computed from the preceding window of data.
    """
    for w in windows:
        df[f'{target_col}_mean_{w}'] = df[target_col].rolling(window=w).mean()
        df[f'{target_col}_std_{w}']  = df[target_col].rolling(window=w).std()
        df[f'{target_col}_min_{w}']  = df[target_col].rolling(window=w).min()
        df[f'{target_col}_max_{w}']  = df[target_col].rolling(window=w).max()
        df[f'{target_col}_sum_{w}']  = df[target_col].rolling(window=w).sum()
    return df

14.3 Signal Processing and Digital Filters

In signal processing, the Moving Average Filter is the simplest low-pass filter, and it is implemented as a sliding window over the signal samples. It smooths out high-frequency noise while preserving low-frequency trends.

The Convolution operation in neural networks (CNNs) is conceptually a sliding window that applies a filter kernel over the input signal or image.

1D convolution = sliding window dot product:

Input signal: [a, b, c, d, e, f]
Kernel:       [k1, k2, k3]

Output[0] = a*k1 + b*k2 + c*k3   (window at position 0)
Output[1] = b*k1 + c*k2 + d*k3   (window slides to position 1)
Output[2] = c*k1 + d*k2 + e*k3   (window slides to position 2)
...

This is exactly a fixed-size sliding window computing a dot product at each position. Every GPU-accelerated deep learning convolution operation is, at its mathematical core, a sliding window.

14.4 Anomaly Detection

Online anomaly detection algorithms maintain a sliding window of recent values and flag data points that deviate significantly from the window's statistics:

  • Z-score anomaly detection: Flag if |x - window_mean| > 3 * window_std
  • Isolation Forest with sliding windows: Retrain periodically on the most recent N data points
  • CUSUM (Cumulative Sum): Detects sustained shifts in a time series using a sliding reference window

14.5 Natural Language Processing

Sliding window in NLP:

  • N-gram extraction: Extracting sequences of N consecutive words (n-grams) from text using a fixed-size sliding window
  • Tokenization context windows: In transformer models, attention is computed within a local context window (sliding attention)
  • Chunking long documents: Splitting documents into overlapping chunks for embedding with a sliding window to preserve context at boundaries

15. Common Pitfalls and How to Avoid Them

Pitfall 1: Off-by-One Errors in Window Boundaries

Wrong (window size ends up k+1):

for (int right = k; right <= n; right++) {  // should be right < n

Wrong (outgoing element index is wrong):

windowState -= arr[right - k + 1]; // should be arr[right - k]

Rule: When right has just moved to index k (0-indexed), the window [0..k] has k+1 elements. The outgoing element is at index right - k = 0. Always double-check by tracing through a small example.

Pitfall 2: Applying Sliding Window to Non-Monotonic Constraints

Trap: "Find the longest subarray with sum equal to target" with a mix of positive and negative numbers.

Why it fails: When you add a negative number, the sum decreases. When you remove an element from the left, the sum may increase or decrease. The monotonicity assumption is broken. You cannot safely move pointers in one direction only.

Solution: Use prefix sums with a hash map for this class of problems.

# Correct approach for sum == target with mixed positive/negative numbers
def subarray_sum_equals_k(nums: list[int], k: int) -> int:
    prefix_sum_count = {0: 1}
    current_sum = 0
    count = 0
 
    for num in nums:
        current_sum += num
        count += prefix_sum_count.get(current_sum - k, 0)
        prefix_sum_count[current_sum] = prefix_sum_count.get(current_sum, 0) + 1
 
    return count

Pitfall 3: Forgetting to Handle the Edge Case Where Left Overtakes Right

In certain implementations, aggressive shrinking can cause left > right. Always ensure your shrinking condition prevents this:

while (left <= right && isInvalid(windowState)) {
    // shrink
}

Pitfall 4: Incorrect State Update on Shrink

When you remove arr[left] from the window, the state update must be the exact inverse of the expansion update. A common mistake is using delete when you should use decrement.

# WRONG: deletes the key even if count > 1
del freq[arr[left]]
 
# CORRECT: decrement and only delete when count reaches 0
freq[arr[left]] -= 1
if freq[arr[left]] == 0:
    del freq[arr[left]]

Pitfall 5: Using a While Loop Instead of If in Fixed-Size Window

In a fixed-size window, the window size is always exactly k. You never need to shrink more than one element at a time. Use if (or just compute directly), not while:

// CORRECT for fixed-size window - single removal
if (right >= k) {
    windowState -= arr[right - k];
}

Pitfall 6: Initializing the First Window Incorrectly

Some implementations forget to build the first window before entering the sliding loop, leading to an incorrect initial state.

Pitfall 7: Counting Subarrays Incorrectly

When counting the number of valid subarrays, the formula is right - left + 1 (not right - left). This represents all subarrays ending at right that start anywhere from left to right.

Pitfall 8: Overflow in Product Windows

When computing products in a sliding window, values can easily overflow 32-bit integers. Always use long in Java or be aware of Python's arbitrary precision integers.


16. Interview Preparation - Strategy and Patterns

16.1 The Recognition Framework

In an interview, when you see a problem, run through this checklist mentally:

1. Is the data linear (array or string)?          -> Yes?
2. Do I need a contiguous portion (subarray/substring)? -> Yes?
3. Is there a constraint or optimization goal?    -> Yes?
4. Are all values non-negative (for sum problems)? -> If Yes: sliding window likely works
                                                      If No: consider prefix sums
5. Is the window size fixed or variable?

16.2 Communication Strategy in Interviews

Follow this order when explaining your approach:

  1. Classify the pattern: "This looks like a sliding window problem because I need a contiguous subarray with an optimization over a constraint."
  2. State window type: "The window size is variable - I'll expand the right pointer and shrink the left pointer when the constraint is violated."
  3. Define the state: "I'll maintain a frequency map / sum / product / count as my window state."
  4. Explain the invariant: "The invariant is that the window [left, right] always satisfies the constraint after the shrinking step."
  5. Code it up: Follow the template, naming variables clearly.
  6. Trace through an example: Walk through your solution on the example input.
  7. Analyze complexity: "The right pointer moves n times, the left pointer moves at most n times total across all iterations. Each step is O(1). Total: O(n) time, O(k) space."

16.3 Follow-up Questions Interviewers Ask

"What if the array has negative numbers?"
Answer: The sum constraint is no longer monotonic, so pure sliding window may not work. Consider prefix sums with a hash map (for exact sum), or a sorted structure.

"Can you do it in O(1) space?"
For frequency-map-based problems, if the alphabet is bounded (e.g., only lowercase letters), replace the hash map with an array of size 26. This is technically O(1) space.

"What if the stream is infinite?"
The sliding window model is perfect for streams - process one element at a time, maintain a bounded window state, never need to store the full history.

"How would you parallelize this?"
Divide the array into chunks. Each thread processes a chunk. Handle boundary windows at chunk seams with communication between adjacent threads. This is how SIMD vectorized implementations work.

16.4 The Template for "At Least K" Problems

"At least k" problems are less common but require a specific variant. The key insight:

count(at least k) = count(all) - count(at most k-1)

Where count(all) = n * (n+1) / 2 (total number of subarrays) for an array of size n.


17. Problem Catalog by Difficulty

Easy

ProblemPatternKey Insight
Max sum subarray of size kFixed window, sumClassic O(1) state
Max average subarray of length kFixed window, averageSame as sum divided by k
First negative integer in every window of size kFixed window, dequeTrack indices of negatives
Count distinct elements in every windowFixed window, frequency mapMaintain count of distinct

Medium

ProblemPatternKey Insight
Longest substring no repeat (LC 3)Variable, maximizeHashMap for last seen index
Longest k distinct (LC 340)Variable, maximizeHashMap frequency, shrink on size > k
Minimum window substring (LC 76)Variable, minimizeTwo frequency maps, formed counter
Find all anagrams (LC 438)Fixed window, frequencymatches counter trick
Longest char replacement (LC 424)Variable, maximizeWindow - maxFreq <= k
Product less than k (LC 713)Variable, countAdd (right-left+1) subarrays per step
Min size subarray sum (LC 209)Variable, minimizeShrink while valid
Permutation in string (LC 567)Fixed window, frequencySame as anagram variant
Max consecutive ones III (LC 1004)Variable, maximizeCount zeros, at most k zeros

Hard

ProblemPatternKey Insight
Sliding window maximum (LC 239)Fixed window, monotonic dequeDeque stores descending indices
Min window substring (LC 76)Variable, minimizeCanonical hard example
Subarrays with k distinct (LC 992)Variable, exactly kAtMostK(k) - AtMostK(k-1)
Sliding window median (LC 480)Fixed window, two heapsLazy deletion for efficiency
Longest substring with at most 2 distinct (LC 159)Variable, maximizeGeneralization of LC 340
Minimum number of K consecutive bit flips (LC 995)Fixed window, greedyFlip tracking with deque

18. Full Implementations in Java and Python

18.1 Comprehensive Java Class with All Major Patterns

import java.util.*;
 
/**
 * Comprehensive Sliding Window implementations covering all major patterns.
 * Each method is a standalone, production-quality solution.
 */
public class SlidingWindowSolutions {
 
    // =======================================================================<mark class="obsidian-highlight">
    // FIXED-SIZE WINDOW PROBLEMS
    // </mark>=====================================================================<mark class="obsidian-highlight">
 
    /**
     * Maximum sum of any contiguous subarray of size k.
     * Time: O(n), Space: O(1)
     */
    public static int maxSumSubarrayK(int[] arr, int k) {
        if (arr </mark> null || arr.length < k || k <= 0) {
            throw new IllegalArgumentException("Invalid input");
        }
 
        int sum = 0;
        for (int i = 0; i < k; i++) sum += arr[i];
 
        int max = sum;
        for (int right = k; right < arr.length; right++) {
            sum += arr[right] - arr[right - k];
            max = Math.max(max, sum);
        }
        return max;
    }
 
    /**
     * All anagrams of pattern p in string s. Returns start indices.
     * Time: O(n), Space: O(1) - bounded alphabet
     */
    public static List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList<>();
        if (s.length() < p.length()) return result;
 
        int[] pFreq = new int[26];
        int[] sFreq = new int[26];
        int k = p.length();
 
        for (int i = 0; i < k; i++) {
            pFreq[p.charAt(i) - 'a']++;
            sFreq[s.charAt(i) - 'a']++;
        }
 
        int matches = 0;
        for (int i = 0; i < 26; i++) {
            if (pFreq[i] <mark class="obsidian-highlight"> sFreq[i]) matches++;
        }
        if (matches </mark> 26) result.add(0);
 
        for (int right = k; right < s.length(); right++) {
            int in = s.charAt(right) - 'a';
            sFreq[in]++;
            matches += (sFreq[in] <mark class="obsidian-highlight"> pFreq[in]) ? 1 : (sFreq[in] </mark> pFreq[in] + 1) ? -1 : 0;
 
            int out = s.charAt(right - k) - 'a';
            sFreq[out]--;
            matches += (sFreq[out] <mark class="obsidian-highlight"> pFreq[out]) ? 1 : (sFreq[out] </mark> pFreq[out] - 1) ? -1 : 0;
 
            if (matches == 26) result.add(right - k + 1);
        }
        return result;
    }
 
    /**
     * Maximum in each window of size k using monotonic deque.
     * Time: O(n), Space: O(k)
     */
    public static int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int[] result = new int[n - k + 1];
        Deque<Integer> deque = new ArrayDeque<>();
 
        for (int right = 0; right < n; right++) {
            while (!deque.isEmpty() && deque.peekFirst() < right - k + 1) {
                deque.pollFirst();
            }
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[right]) {
                deque.pollLast();
            }
            deque.offerLast(right);
            if (right >= k - 1) {
                result[right - k + 1] = nums[deque.peekFirst()];
            }
        }
        return result;
    }
 
    // =======================================================================<mark class="obsidian-highlight">
    // VARIABLE-SIZE WINDOW - MAXIMIZE LENGTH
    // </mark>=======================================================================
 
    /**
     * Longest substring without repeating characters.
     * Time: O(n), Space: O(min(n, 128))
     */
    public static int lengthOfLongestSubstring(String s) {
        int[] lastIndex = new int[128]; // ASCII
        Arrays.fill(lastIndex, -1);
        int left = 0;
        int max = 0;
 
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            if (lastIndex[c] >= left) {
                left = lastIndex[c] + 1;
            }
            lastIndex[c] = right;
            max = Math.max(max, right - left + 1);
        }
        return max;
    }
 
    /**
     * Longest substring with at most k distinct characters.
     * Time: O(n), Space: O(k)
     */
    public static int lengthOfLongestSubstringKDistinct(String s, int k) {
        if (k == 0) return 0;
        Map<Character, Integer> freq = new HashMap<>();
        int left = 0, max = 0;
 
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            freq.merge(c, 1, Integer::sum);
 
            while (freq.size() > k) {
                char lc = s.charAt(left++);
                freq.merge(lc, -1, Integer::sum);
                if (freq.get(lc) == 0) freq.remove(lc);
            }
            max = Math.max(max, right - left + 1);
        }
        return max;
    }
 
    /**
     * Longest subarray with at most k zeros (max consecutive ones with k flips).
     * Time: O(n), Space: O(1)
     */
    public static int longestOnes(int[] nums, int k) {
        int left = 0, zeros = 0, max = 0;
 
        for (int right = 0; right < nums.length; right++) {
            if (nums[right] <mark class="obsidian-highlight"> 0) zeros++;
 
            while (zeros > k) {
                if (nums[left] </mark> 0) zeros--;
                left++;
            }
            max = Math.max(max, right - left + 1);
        }
        return max;
    }
 
    /**
     * Longest substring with character replacement (at most k replacements).
     * Time: O(n), Space: O(1)
     */
    public static int characterReplacement(String s, int k) {
        int[] freq = new int[26];
        int left = 0, maxFreq = 0, max = 0;
 
        for (int right = 0; right < s.length(); right++) {
            maxFreq = Math.max(maxFreq, ++freq[s.charAt(right) - 'A']);
            if (right - left + 1 - maxFreq > k) {
                freq[s.charAt(left++) - 'A']--;
            }
            max = Math.max(max, right - left + 1);
        }
        return max;
    }
 
    // =======================================================================<mark class="obsidian-highlight">
    // VARIABLE-SIZE WINDOW - MINIMIZE LENGTH
    // </mark>=======================================================================
 
    /**
     * Minimum length subarray with sum >= target.
     * Time: O(n), Space: O(1)
     */
    public static int minSubArrayLen(int target, int[] nums) {
        int left = 0, sum = 0, min = Integer.MAX_VALUE;
 
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];
            while (sum >= target) {
                min = Math.min(min, right - left + 1);
                sum -= nums[left++];
            }
        }
        return min == Integer.MAX_VALUE ? 0 : min;
    }
 
    /**
     * Minimum window in s containing all characters of t.
     * Time: O(n + m), Space: O(m)
     */
    public static String minWindow(String s, String t) {
        if (s.isEmpty() || t.isEmpty()) return "";
 
        Map<Character, Integer> req = new HashMap<>();
        for (char c : t.toCharArray()) req.merge(c, 1, Integer::sum);
 
        int required = req.size(), formed = 0;
        Map<Character, Integer> win = new HashMap<>();
        int left = 0, minLen = Integer.MAX_VALUE, minStart = 0;
 
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            win.merge(c, 1, Integer::sum);
            if (req.containsKey(c) && win.get(c).equals(req.get(c))) formed++;
 
            while (formed == required) {
                if (right - left + 1 < minLen) {
                    minLen = right - left + 1;
                    minStart = left;
                }
                char lc = s.charAt(left++);
                win.merge(lc, -1, Integer::sum);
                if (req.containsKey(lc) && win.get(lc) < req.get(lc)) formed--;
            }
        }
        return minLen <mark class="obsidian-highlight"> Integer.MAX_VALUE ? "" : s.substring(minStart, minStart + minLen);
    }
 
    // </mark>=====================================================================<mark class="obsidian-highlight">
    // COUNTING SUBARRAYS
    // </mark>=======================================================================
 
    /**
     * Count subarrays with product strictly less than k.
     * Time: O(n), Space: O(1)
     */
    public static int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) return 0;
        int left = 0, product = 1, count = 0;
 
        for (int right = 0; right < nums.length; right++) {
            product *= nums[right];
            while (product >= k) product /= nums[left++];
            count += right - left + 1;
        }
        return count;
    }
 
    /**
     * Count subarrays with exactly k distinct integers.
     * Uses the at-most-k trick.
     * Time: O(n), Space: O(k)
     */
    public static int subarraysWithKDistinct(int[] nums, int k) {
        return atMostKDistinct(nums, k) - atMostKDistinct(nums, k - 1);
    }
 
    private static int atMostKDistinct(int[] nums, int k) {
        Map<Integer, Integer> freq = new HashMap<>();
        int left = 0, count = 0;
 
        for (int right = 0; right < nums.length; right++) {
            freq.merge(nums[right], 1, Integer::sum);
            while (freq.size() > k) {
                freq.merge(nums[left], -1, Integer::sum);
                if (freq.get(nums[left]) == 0) freq.remove(nums[left]);
                left++;
            }
            count += right - left + 1;
        }
        return count;
    }
 
    // =======================================================================<mark class="obsidian-highlight">
    // MAIN: Test runner
    // </mark>=====================================================================<mark class="obsidian-highlight">
 
    public static void main(String[] args) {
        // Test all solutions
        System.out.println("</mark>= Fixed Window =<mark class="obsidian-highlight">");
        System.out.println(maxSumSubarrayK(new int[]{2, 1, 5, 1, 3, 2}, 3));       // 9
        System.out.println(findAnagrams("cbaebabacd", "abc"));                       // [0, 6]
        System.out.println(Arrays.toString(maxSlidingWindow(new int[]{1,3,-1,-3,5,3,6,7}, 3))); // [3,3,5,5,6,7]
 
        System.out.println("\n</mark>= Variable Window - Maximize =<mark class="obsidian-highlight">");
        System.out.println(lengthOfLongestSubstring("abcabcbb"));                    // 3
        System.out.println(lengthOfLongestSubstringKDistinct("eceba", 2));           // 3
        System.out.println(longestOnes(new int[]{1,1,1,0,0,0,1,1,1,1,0}, 2));      // 6
        System.out.println(characterReplacement("AABABBA", 1));                      // 4
 
        System.out.println("\n</mark>= Variable Window - Minimize =<mark class="obsidian-highlight">");
        System.out.println(minSubArrayLen(7, new int[]{2,3,1,2,4,3}));              // 2
        System.out.println(minWindow("ADOBECODEBANC", "ABC"));                       // "BANC"
 
        System.out.println("\n</mark>= Counting =<mark class="obsidian-highlight">");
        System.out.println(numSubarrayProductLessThanK(new int[]{10,5,2,6}, 100));  // 8
        System.out.println(subarraysWithKDistinct(new int[]{1,2,1,2,3}, 2));        // 7
    }
}

18.2 Comprehensive Python Module

"""
sliding_window.py
</mark>=============<mark class="obsidian-highlight">
Comprehensive sliding window implementations covering all major patterns.
Each function is standalone and production-quality.
"""
 
from collections import defaultdict, deque
from typing import Optional
 
 
# </mark>=========================================================================<mark class="obsidian-highlight">
# FIXED-SIZE WINDOW PROBLEMS
# </mark>===========================================================================
 
def max_sum_subarray_k(arr: list[int], k: int) -> int:
    """Maximum sum of any contiguous subarray of size k.
    Time: O(n), Space: O(1)
    """
    if not arr or len(arr) < k or k <= 0:
        raise ValueError("Invalid input")
 
    window_sum = sum(arr[:k])
    max_sum = window_sum
 
    for right in range(k, len(arr)):
        window_sum += arr[right] - arr[right - k]
        max_sum = max(max_sum, window_sum)
 
    return max_sum
 
 
def find_anagrams(s: str, p: str) -> list[int]:
    """All start indices of anagram occurrences of p in s.
    Time: O(n), Space: O(1)
    """
    from collections import Counter
    result = []
    if len(s) < len(p):
        return result
 
    k = len(p)
    p_count = Counter(p)
    s_count = Counter(s[:k])
 
    if s_count == p_count:
        result.append(0)
 
    for right in range(k, len(s)):
        s_count[s[right]] += 1
        outgoing = s[right - k]
        s_count[outgoing] -= 1
        if s_count[outgoing] <mark class="obsidian-highlight"> 0:
            del s_count[outgoing]
        if s_count </mark> p_count:
            result.append(right - k + 1)
 
    return result
 
 
def max_sliding_window(nums: list[int], k: int) -> list[int]:
    """Maximum element in each window of size k.
    Uses monotonic deque. Time: O(n), Space: O(k)
    """
    result = []
    dq = deque()  # stores indices in decreasing order of nums values
 
    for right in range(len(nums)):
        # Remove out-of-window indices
        if dq and dq[0] < right - k + 1:
            dq.popleft()
 
        # Remove indices with smaller values from the back
        while dq and nums[dq[-1]] < nums[right]:
            dq.pop()
 
        dq.append(right)
 
        if right >= k - 1:
            result.append(nums[dq[0]])
 
    return result
 
 
# ===========================================================================<mark class="obsidian-highlight">
# VARIABLE-SIZE WINDOW - MAXIMIZE LENGTH
# </mark>===========================================================================
 
def length_of_longest_substring(s: str) -> int:
    """Longest substring without repeating characters.
    Time: O(n), Space: O(min(n, alphabet))
    """
    char_index = {}
    left = 0
    max_length = 0
 
    for right, c in enumerate(s):
        if c in char_index and char_index[c] >= left:
            left = char_index[c] + 1
        char_index[c] = right
        max_length = max(max_length, right - left + 1)
 
    return max_length
 
 
def longest_k_distinct(s: str, k: int) -> int:
    """Longest substring with at most k distinct characters.
    Time: O(n), Space: O(k)
    """
    if k == 0:
        return 0
 
    freq = defaultdict(int)
    left = 0
    max_length = 0
 
    for right, c in enumerate(s):
        freq[c] += 1
 
        while len(freq) > k:
            left_char = s[left]
            freq[left_char] -= 1
            if freq[left_char] == 0:
                del freq[left_char]
            left += 1
 
        max_length = max(max_length, right - left + 1)
 
    return max_length
 
 
def longest_ones(nums: list[int], k: int) -> int:
    """Longest subarray of 1s with at most k zeros flipped.
    Time: O(n), Space: O(1)
    """
    left = 0
    zeros = 0
    max_length = 0
 
    for right, num in enumerate(nums):
        if num == 0:
            zeros += 1
 
        while zeros > k:
            if nums[left] == 0:
                zeros -= 1
            left += 1
 
        max_length = max(max_length, right - left + 1)
 
    return max_length
 
 
def character_replacement(s: str, k: int) -> int:
    """Longest substring with at most k character replacements to make uniform.
    Time: O(n), Space: O(1)
    """
    freq = [0] * 26
    left = 0
    max_freq = 0
    max_length = 0
 
    for right, c in enumerate(s):
        idx = ord(c) - ord('A')
        freq[idx] += 1
        max_freq = max(max_freq, freq[idx])
 
        if (right - left + 1) - max_freq > k:
            freq[ord(s[left]) - ord('A')] -= 1
            left += 1
 
        max_length = max(max_length, right - left + 1)
 
    return max_length
 
 
# ===========================================================================<mark class="obsidian-highlight">
# VARIABLE-SIZE WINDOW - MINIMIZE LENGTH
# </mark>===========================================================================
 
def min_subarray_len(target: int, nums: list[int]) -> int:
    """Minimum length contiguous subarray with sum >= target.
    Time: O(n), Space: O(1)
    """
    left = 0
    current_sum = 0
    min_length = float('inf')
 
    for right, num in enumerate(nums):
        current_sum += num
 
        while current_sum >= target:
            min_length = min(min_length, right - left + 1)
            current_sum -= nums[left]
            left += 1
 
    return 0 if min_length == float('inf') else min_length
 
 
def min_window(s: str, t: str) -> str:
    """Minimum window in s that contains all characters of t.
    Time: O(n + m), Space: O(m)
    """
    if not s or not t:
        return ""
 
    required = defaultdict(int)
    for c in t:
        required[c] += 1
 
    total_required = len(required)
    formed = 0
    window_counts = defaultdict(int)
    left = 0
    min_len = float('inf')
    min_start = 0
 
    for right, c in enumerate(s):
        window_counts[c] += 1
 
        if c in required and window_counts[c] == required[c]:
            formed += 1
 
        while formed == total_required:
            if right - left + 1 < min_len:
                min_len = right - left + 1
                min_start = left
 
            left_char = s[left]
            window_counts[left_char] -= 1
            if left_char in required and window_counts[left_char] < required[left_char]:
                formed -= 1
            left += 1
 
    return "" if min_len <mark class="obsidian-highlight"> float('inf') else s[min_start:min_start + min_len]
 
 
# </mark>=========================================================================<mark class="obsidian-highlight">
# COUNTING SUBARRAYS
# </mark>===========================================================================
 
def num_subarray_product_less_than_k(nums: list[int], k: int) -> int:
    """Count subarrays with product strictly less than k.
    Time: O(n), Space: O(1)
    """
    if k <= 1:
        return 0
 
    left = 0
    product = 1
    count = 0
 
    for right, num in enumerate(nums):
        product *= num
 
        while product >= k:
            product //= nums[left]
            left += 1
 
        count += right - left + 1
 
    return count
 
 
def _at_most_k_distinct(nums: list[int], k: int) -> int:
    """Helper: count subarrays with at most k distinct elements."""
    freq = defaultdict(int)
    left = 0
    count = 0
 
    for right, num in enumerate(nums):
        freq[num] += 1
 
        while len(freq) > k:
            freq[nums[left]] -= 1
            if freq[nums[left]] == 0:
                del freq[nums[left]]
            left += 1
 
        count += right - left + 1
 
    return count
 
 
def subarrays_with_k_distinct(nums: list[int], k: int) -> int:
    """Count subarrays with exactly k distinct elements.
    Uses the at-most-k trick: exactly(k) = atMost(k) - atMost(k-1)
    Time: O(n), Space: O(k)
    """
    return _at_most_k_distinct(nums, k) - _at_most_k_distinct(nums, k - 1)
 
 
# ===========================================================================<mark class="obsidian-highlight">
# CIRCUIT BREAKER SLIDING WINDOW
# </mark>===========================================================================
 
class CircuitBreaker:
    """
    Count-based sliding window circuit breaker.
    Tracks the last `window_size` call results.
    Opens the circuit when failure rate exceeds `failure_threshold`.
    """
 
    from enum import Enum
 
    class State:
        CLOSED = "CLOSED"
        OPEN = "OPEN"
        HALF_OPEN = "HALF_OPEN"
 
    def __init__(self, window_size: int, failure_threshold: float, open_duration_seconds: float):
        import time
        self.window_size = window_size
        self.failure_threshold = failure_threshold
        self.open_duration_seconds = open_duration_seconds
        self.state = self.State.CLOSED
        self.window: deque = deque()
        self.failure_count = 0
        self.opened_at: Optional[float] = None
        self._time = time
 
    def allow_request(self) -> bool:
        if self.state == self.State.OPEN:
            elapsed = self._time.time() - self.opened_at
            if elapsed >= self.open_duration_seconds:
                self.state = self.State.HALF_OPEN
                return True
            return False
        return True
 
    def record_success(self):
        self._record(False)
        if self.state == self.State.HALF_OPEN:
            self.state = self.State.CLOSED
            self.window.clear()
            self.failure_count = 0
 
    def record_failure(self):
        self._record(True)
 
    def _record(self, is_failure: bool):
        if self.state == self.State.OPEN:
            return
 
        # Sliding window: add new result
        self.window.append(is_failure)
        if is_failure:
            self.failure_count += 1
 
        # Evict oldest if window is full
        if len(self.window) > self.window_size:
            evicted = self.window.popleft()
            if evicted:
                self.failure_count -= 1
 
        # Evaluate only when window is full
        if len(self.window) == self.window_size:
            failure_rate = self.failure_count / self.window_size
            if failure_rate >= self.failure_threshold:
                self.state = self.State.OPEN
                self.opened_at = self._time.time()
 
 
# ===========================================================================<mark class="obsidian-highlight">
# TESTS
# </mark>=========================================================================<mark class="obsidian-highlight">
 
def run_all_tests():
    print("</mark>= Fixed-Size Window =<mark class="obsidian-highlight">")
    assert max_sum_subarray_k([2, 1, 5, 1, 3, 2], 3) </mark> 9
    assert find_anagrams("cbaebabacd", "abc") <mark class="obsidian-highlight"> [0, 6]
    assert max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3) </mark> [3, 3, 5, 5, 6, 7]
    print("All fixed-size window tests passed.")
 
    print("\n=<mark class="obsidian-highlight"> Variable Window - Maximize </mark>=")
    assert length_of_longest_substring("abcabcbb") <mark class="obsidian-highlight"> 3
    assert length_of_longest_substring("bbbbb") </mark> 1
    assert longest_k_distinct("eceba", 2) <mark class="obsidian-highlight"> 3
    assert longest_ones([1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], 2) </mark> 6
    assert character_replacement("AABABBA", 1) <mark class="obsidian-highlight"> 4
    print("All variable maximize tests passed.")
 
    print("\n</mark>= Variable Window - Minimize =<mark class="obsidian-highlight">")
    assert min_subarray_len(7, [2, 3, 1, 2, 4, 3]) </mark> 2
    assert min_window("ADOBECODEBANC", "ABC") <mark class="obsidian-highlight"> "BANC"
    assert min_window("a", "aa") </mark> ""
    print("All variable minimize tests passed.")
 
    print("\n=<mark class="obsidian-highlight"> Counting </mark>=")
    assert num_subarray_product_less_than_k([10, 5, 2, 6], 100) <mark class="obsidian-highlight"> 8
    assert subarrays_with_k_distinct([1, 2, 1, 2, 3], 2) </mark> 7
    print("All counting tests passed.")
 
    print("\nAll tests passed!")
 
 
if __name__ == "__main__":
    run_all_tests()

19. Complexity Cheat Sheet

Problem TypeTime ComplexitySpace ComplexityNotes
Fixed window, scalar state (sum, product)O(n)O(1)Ideal case
Fixed window, frequency mapO(n)O(alphabet size)Bounded alphabet -> O(1) space
Fixed window, monotonic dequeO(n) amortizedO(k)Each element enters/exits deque once
Variable window, scalar stateO(n)O(1)Left and right each move n steps total
Variable window, frequency mapO(n)O(distinct elements)Map operations are O(1) amortized
Variable window, two heapsO(n log k)O(k)Heap operations are O(log k)
Variable window, TreeMapO(n log k)O(k)For order statistics beyond min/max
"Exactly k" via at-most trickO(n)O(k)Two passes of at-most template

20. Decision Tree - Which Window Type to Use

START: Linear sequence problem with subarray/substring constraint
                            |
                            v
              Is window size fixed (given k)?
             /                              \
           YES                              NO
            |                               |
     Use fixed-size                Are you maximizing length?
     window template               /                       \
            |                    YES                       NO
     Do you need                  |                         |
     min/max in window?     Expand right freely      Are you minimizing?
     /             \        Shrink left when          /              \
   YES              NO      constraint violated      YES              NO
    |                |             |                  |               |
  Monotonic      Scalar/    Answer = max        Shrink left       Count valid
  deque O(n)     map O(n)  window size         while valid       subarrays:
                           seen so far         Answer = min      Add (right-
                                               window size       left+1) each
                                               seen              valid step
                                                    |
                                         Do you need "exactly k"?
                                         /                      \
                                        YES                      NO
                                         |                       |
                                  AtMost(k) -             Standard variable
                                  AtMost(k-1)             window template

Summary: The Five Laws of Sliding Window

Law 1 - The Monotonicity Law: Sliding window works only when the constraint changes monotonically as you expand or shrink the window. Verify this before applying the pattern.

Law 2 - The Two-Pointer Law: Every sliding window algorithm uses two pointers - left and right. Right only moves forward. Left only moves forward. Neither ever moves backward. This guarantees O(n) complexity.

Law 3 - The State Law: Always maintain the window state incrementally. Adding an element to the window and removing an element from the window must each be O(1) or O(log n) operations. If it requires O(k) to update the state, you have not properly chosen the state data structure.

Law 4 - The Invariant Law: After the shrinking step, the window must always satisfy the required invariant (valid constraint). Write your shrinking condition as a while loop, not an if, to ensure this invariant is restored completely.

Law 5 - The Answer-Update Law: Know precisely when to update the answer - for fixed windows, after the window reaches size k; for variable maximize, after each shrink; for variable minimize, during the shrink; for counting, at every step.


The sliding window is elegant because it encodes a deep truth: in sequential data with monotonic constraints, you never need to look backwards. The past is captured in the window state. The future is irrelevant. Only the present window matters.


Last updated: May 2026 | Covers LeetCode patterns, system design applications, and production engineering use cases