What is a Bloom Filter?
A Bloom Filter is a probabilistic data structure used to efficiently determine whether an element is a member of a set. It is designed to be extremely fast and highly memory-efficient, making it ideal for applications that need to handle large datasets while using minimal memory.
A Bloom Filter can return two possible results:
Definitely Not Present – If the Bloom Filter indicates that an element is not in the set, it is guaranteed to be absent. False negatives are impossible.
Possibly Present – If the Bloom Filter indicates that an element is in the set, the element may or may not actually exist in the set. This is because Bloom Filters can produce false positives.
In simple terms:
No false negatives – If it says "not present", the element is definitely not in the set.
Possible false positives – If it says "present", the element might actually be present, or it could be a false positive.
Because of this trade-off, Bloom Filters are widely used in systems where speed and memory efficiency are more important than perfect accuracy, such as databases, caches, web browsers, distributed systems, and networking applications.
1. Clarifying Requirements
Before starting the design, it is important to clarify the requirements by asking relevant questions. This helps uncover hidden assumptions, resolve ambiguities, and define the scope of the system. Gathering these details early ensures that the design aligns with the interviewer's expectations and avoids making incorrect assumptions.
The following is an example of a discussion between the candidate and the interviewer during the requirement clarification phase:
Candidate: What type of elements will the Bloom Filter store? Should it support only strings, or also integers and arbitrary objects?
Interviewer: Let's keep it simple and support strings. That covers most real-world use cases, such as URLs, usernames, email addresses, and API keys.
Candidate: What operations should the Bloom Filter provide? Should it only support add()and mightContain(), or should it also support deletion?
Interviewer: A standard Bloom Filter supports only add()and mightContain(). Deletion is not required because it can lead to false negatives. If deletion is needed, a Counting Bloom Filter would be a better choice.
Candidate: Should the desired false positive rate be configurable, or should we use a fixed value?
Interviewer: The false positive rate should be configurable by the caller. If no value is provided, use a sensible default, such as 1% (0.01).
Candidate: Will this Bloom Filter be used in a multi-threaded environment? Should it be thread-safe?
Interviewer: Yes. Assume multiple threads may add elements and check membership concurrently, such as in a web service that maintains a URL blacklist or cache.
Candidate: Should the caller be able to choose the hash function or hashing algorithm?
Interviewer: Yes. The design should allow the hash function to be configurable or pluggable, so different applications can choose the algorithm that best fits their performance and distribution requirements.
Based on the discussion, we can summarize the system requirements as follows:
Functional Requirements
Support an
add(element)operation to insert a string into the Bloom Filter.Support a
mightContain(element)operation that:Returns
falseif the element is definitely not in the set.Returns
trueif the element may be in the set.
Support a
clear()operation to reset the Bloom Filter to its initial empty state.Automatically calculate the optimal bit array size and the optimal number of hash functions based on:
Expected number of elements (
n)Desired false positive probability (
p)
Allow the caller to choose or plug in a hash function strategy.
Non-Functional Requirements
Performance: Both
add()andmightContain()should execute in O(k) time, wherekis the number of hash functions.Space Efficiency: The Bloom Filter should use significantly less memory than storing all elements explicitly.
Thread Safety: The implementation must safely support concurrent
add()andmightContain()operations.Configurability: The caller should be able to configure:
Expected number of elements
Desired false positive rate
Hash function strategy
With the requirements clearly defined, the next step is to identify the core building blocks and design the classes that make up our Bloom Filter system.
2. Identifying Core Components
Unlike systems that model real-world entities such as users, orders, or bookings, a Bloom Filter is a data structure design problem. Instead of modeling business objects, our goal is to identify the internal components that work together to provide fast, memory-efficient membership testing while maintaining a configurable false positive rate.
2.1 Why Do We Need a Bloom Filter?
Requirement:
The filter should use significantly less memory than storing all elements explicitly.
The simplest way to check whether an element exists is to use a HashSet.
A HashSet provides:
- O(1) average-time insertion and lookup.
- No false positives or false negatives.
- Stores every element explicitly.
So why not always use a HashSet?
The drawback is memory usage. Since every element is stored, memory consumption grows linearly with the number of elements. For example, storing 10 million URLs in a HashSet can require hundreds of megabytes of memory.
A Bloom Filter solves this problem by storing only a compact bit array instead of the actual elements. This makes it significantly more memory-efficient.
The trade-off is:
- No false negatives — if the filter says an element is not present, it is guaranteed to be absent.
- Possible false positives — if the filter says an element may be present, it could still be absent.
2.2 How Does a Bit Array Enable Membership Testing?
The core component of a Bloom Filter is a bit array, which is a fixed-size array where each position stores either 0 or 1.
Adding an Element
When an element is inserted:
- Compute k hash values for the element.
- Each hash maps to a position in the bit array.
- Set all corresponding bits to 1.
Checking Membership
When checking whether an element exists:
- Compute the same k hash values.
- Check the corresponding bit positions.
- If any bit is 0, the element was definitely never added.
- If all bits are 1, the element may be present.
This works because multiple elements can set the same bits, which is what introduces the possibility of false positives.
2.3 Why Are Multiple Hash Functions Needed?
Using only one hash function would cause many elements to map to the same bit positions, resulting in a high collision rate and frequent false positives.
Instead, a Bloom Filter uses k independent hash functions.
Each element is mapped to k different positions in the bit array.
For an element that was never added to produce a false positive, all k corresponding bits must already have been set by other elements.
As the number of independent hash functions increases, the probability of all these bits being set accidentally decreases significantly (up to an optimal value of k), reducing the false positive rate.
This combination of a bit array and multiple hash functions enables Bloom Filters to provide fast, memory-efficient, probabilistic membership testing.
2.4 Entity overview
Core Components
| Component | Type | Responsibility |
|---|---|---|
| HashStrategy | Interface | Defines the contract for generating hash values and mapping an element to one or more bit positions. |
| MurmurHashStrategy | Implementation | Implements a MurmurHash-inspired algorithm that provides fast hashing with excellent distribution. |
| FNVHashStrategy | Implementation | Implements the FNV-1a hashing algorithm, which is simple, fast, and lightweight. |
| DJB2HashStrategy | Implementation | Implements the DJB2 hashing algorithm, offering a lightweight alternative for generating hash values. |
| BitArray | Data Class | Encapsulates a fixed-size bit array and provides efficient set(), get(), and clear() operations. |
| BloomFilterConfig | Data Class | Stores the immutable configuration of the Bloom Filter, including the bit array size, number of hash functions, expected element count, and false positive probability. |
| BloomFilter | Core Class | Coordinates the bit array and hash strategy to perform probabilistic membership testing through add() and mightContain() operations. |
| BloomFilter.Builder | Builder | Constructs and configures a BloomFilter instance by calculating optimal parameters and applying sensible defaults. |
Design Summary
These components form the core architecture of our Bloom Filter. Each component has a well-defined responsibility, resulting in a modular and extensible design.
- Hashing is abstracted behind the
HashStrategyinterface, allowing different hashing algorithms to be plugged in without modifying the Bloom Filter. - Configuration is computed once during construction and stored in an immutable
BloomFilterConfigobject. - The BitArray efficiently stores the filter's state using a compact sequence of bits.
- The BloomFilter acts as the central coordinator, combining the bit array and hash strategy to provide fast, memory-efficient membership testing.
- The Builder simplifies object creation by computing optimal parameters and exposing a flexible configuration API.
Note
In a real Low-Level Design (LLD) interview, you are not expected to know or implement specific hash algorithms such as MurmurHash, FNV-1a, or DJB2. These implementations are included here to demonstrate a production-quality design.
For an interview, a much simpler approach is sufficient. You can implement a basic polynomial rolling hash (for example,
hash = hash × seed + character) and use different seed values to simulate multiple independent hash functions.What interviewers are evaluating is your design, not your knowledge of hashing algorithms. They expect you to demonstrate:
- The use of the Strategy Design Pattern to make hashing pluggable.
- How multiple independent hash functions reduce the false positive rate.
- How hash values are mapped to valid bit array indices.
- A clean, modular, and extensible object-oriented design.
With the core components identified, the next step is to define their attributes, behaviors, and relationships in the class diagram.
3. Designing Classes and Relationships
Now that we have identified the core components of our Bloom Filter, the next step is to design the classes that implement these components.
For each class, we will define:
- Attributes – the data the class stores.
- Methods – the operations the class performs.
- Relationships – how the class interacts with other classes.
By the end of this section, we'll have a complete object-oriented design that is modular, extensible, and easy to maintain.
Note
To keep the discussion focused on the design, we will omit trivial getter and setter methods and concentrate only on the core behaviors of each class.
3.1 Class Definitions
We'll follow a bottom-up approach, starting with the simplest building blocks and gradually moving toward the main class.
The design will be covered in the following order:
- Interfaces – Define contracts for interchangeable behaviors.
- Data Classes – Store the application's state and configuration.
- Implementations – Provide concrete implementations of the interfaces.
- Core Classes – Coordinate all components to implement the Bloom Filter.
3.1.1 Interface: HashStrategy
A Bloom Filter relies on hash functions to map elements to positions in the bit array.
One option would be to hardcode a single hashing algorithm inside the BloomFilter class. However, different applications have different requirements:
- A URL blacklist may prioritize high performance.
- A spell checker may prioritize better hash distribution to reduce collisions.
- Other applications may require a completely different hashing strategy.
To keep the design flexible and extensible, we introduce the Strategy Design Pattern.
The HashStrategy interface defines a common contract for all hashing algorithms. The BloomFilter depends only on this interface, allowing different hashing implementations to be plugged in without modifying the Bloom Filter itself.
Responsibilities
- Define a common interface for generating hash values.
- Map an element to one or more valid positions in the bit array.
- Allow different hashing algorithms to be used interchangeably.
- Promote extensibility by decoupling the Bloom Filter from specific hash implementations.
This design follows the Open/Closed Principle: the Bloom Filter is open for extension (new hash algorithms can be added) but closed for modification (existing code remains unchanged).
Methods
| Method | Description |
|---|---|
hash(element, seed, bitArraySize) | Computes a hash value for the given element using the specified seed and returns a valid bit position in the range [0, bitArraySize). |
Why Do We Need the seed Parameter?
The seed parameter allows us to generate multiple independent hash values using a single hashing algorithm.
Instead of implementing k different hash functions, we invoke the same hash() method k times, each with a different seed value:
- Seed 0 → First hash value
- Seed 1 → Second hash value
- Seed 2 → Third hash value
- ...
- Seed k − 1 → k<sup>th</sup> hash value
Each seed produces a different hash value for the same element, resulting in k distinct bit positions in the bit array.
This approach keeps the implementation simple while achieving the effect of multiple independent hash functions.
3.1.2 Data Class: BitArray
The BitArray is the storage backbone of the Bloom Filter. It maintains the sequence of bits that records whether particular hash positions have been set.
One option would be to store a raw boolean[] (or BitSet) directly inside the BloomFilter class. However, encapsulating it within a dedicated BitArray class provides a cleaner and more maintainable design.
The BitArray class abstracts low-level bit operations and exposes a simple API for manipulating bits, allowing the BloomFilter to focus solely on coordinating hash functions and membership testing.
Responsibilities
- Store a fixed-size array of bits.
- Set a bit at a specified position.
- Retrieve the value of a bit.
- Clear all bits, resetting the array to its initial state.
- Hide low-level bit manipulation behind a simple, reusable interface.
By separating storage logic from business logic, the design follows the Single Responsibility Principle (SRP) and makes the Bloom Filter easier to understand, test, and maintain.
4. Code Implementation
Now that we have completed the design, the next step is to translate it into a working implementation.
We'll follow a bottom-up approach, implementing the foundational components first and then building the higher-level classes on top of them. This ensures that each component is available before it is referenced by another.
We'll implement the classes in the following order:
- Data Classes – Implement the core data structures (
BitArrayandBloomFilterConfig). - Interfaces – Define the
HashStrategycontract. - Strategy Implementations – Implement concrete hashing algorithms such as
MurmurHashStrategy,FNVHashStrategy, andDJB2HashStrategy. - Core Class – Implement the
BloomFilter, which coordinates all components to provide probabilistic membership testing. - Builder Class – Implement
BloomFilter.Builderto simplify object creation and automatically compute the optimal configuration.
By implementing the system in this order, each layer builds upon the one below it, resulting in a clean, modular, and easy-to-understand implementation.
4.1 Hash Strategies
HashStrategy Interface
The HashStrategy interface defines a common contract for all hashing algorithms. It accepts an element, a seed, and the bit array size, and returns a valid bit position within the range [0, bitArraySize).
MurmurHashStrategy
MurmurHash3 is one of the most widely used non-cryptographic hash functions. It is fast, provides excellent hash distribution, and is the default hashing algorithm used in production Bloom Filter implementations such as Google Guava.
FNVHashStrategy
FNV-1a (Fowler–Noll–Vo) is a simple, fast, and widely used non-cryptographic hash function. Although it is simpler than MurmurHash3, it still provides good hash distribution and is commonly used in hash tables, compilers, and network protocols.
FNVHashStrategy
FNV-1a (Fowler–Noll–Vo) is a simple, fast, and widely used non-cryptographic hash function. Although it is simpler than MurmurHash3, it still provides good hash distribution and is commonly used in hash tables, compilers, and network protocols.