I have written about Bloom Filters before in Bloom Filters Applied In Real Life Application - Laravel Prototype and made a prototype for it using Laravel.
Now I'm very pleased to announce that I finally — and after repeated attempts — finished developing the Selective Package!!
Let's go through the journey I took to make this package.
Transitioning from a Prototype to a Generic Package
I recommend reading my earlier blog Bloom Filters Applied In Real Life Application - Laravel Prototype before continuing with this blog, as it explains the internal mechanism of how Bloom Filters work
In my last blog, when I showed the use case for bloom filters and implemented the prototype, I noticed a lack of support for that solution in the Laravel & PHP ecosystems. So I decided to take the initiative and make this package.
It wasn’t hard to implement that as a reusable package since I already made a prototype and it had the core functionality already working correctly, but I got very busy by then, and that’s one thing a lot of people underestimate.
Time management and prioritizing change a lot of things for us developers often. We don’t get paid to make those packages for public use, so we – unintentionally – give lower priority to working on those kinds of things. And under unpleasant circumstances at work, which turned working into a struggle to keep yourself up and running.
Cheerfully, I finally switched gears, and I was able to manage my time to give some time to the research on this package making.
Research Importance in Making Developer Tools
We normally, when working on a business project in a company, have a product research phase (or whatever you want to call it), in that phase, we go deep into the nature of the market of that product we are trying to introduce to the market.
And also, we research the business domains of that product in detail to make a solid plan for the features we need to develop.
I believe that a similar process should be applied to Developer tools as well, but different in the method, as we are not making a business here, and it’s a solution, not a business domain.
What we need to research is the inclusiveness of the tool more. Naturally, we developers might use the same library/package in a project that deals with delivery systems and the exact same library/package in a CRM, or maybe an HR system, etc…
Having that in mind, making our tool as inclusive and generic as possible is something that will indeed make a noticeable difference in the Developer Experience (DX) for the developer using our tool.
In the case of Selective, that part was adding the bloom:seed command. I assumed the developer could add my library after already having a lot of records in the table, and they might need to add those older records as well in the bit array, so having a command to do that made sense in that case.
Solving Database Uniqueness Strain at Scale
At scale, standard uniqueness validations (unique:users,email) run standard SELECT COUNT(*) or EXISTS queries on your database. If you have 10 million users, even with an index, running these checks on every sign-up, profile update, or request can strain database connections.
Selective inserts a Bloom Filter (using a RedisBloom-backed data structure) between the application layer and your primary database.
-
BloomUnique Rule: When a user registers with an email, Selective queries the Redis Bloom filter
selective:users:email. If the email is not in the filter (100% certainty), validation passes instantly. If it might be in the filter (probabilistic check), Selective falls back to a standard database check to verify. - BloomExists Rule: Similarly, it validates that a record exists (useful for high-volume reference checks).
- Graceful Error Handling & Fallback: If Redis crashes, is misconfigured, or drops, Selective logs a warning and falls back to database validation. Your application suffers zero downtime.
Package Architecture & Key Code Components
1. Core Service: BloomFilterService
The service layer wraps Redis raw command execution. Because Laravel's default Redis wrapper does not natively implement Bloom commands, we use executeRaw() to run commands like BF.RESERVE, BF.INSERT, BF.EXISTS, BF.MADD, and BF.INFO.
Key implementation details:
-
Auto-Reservation: If
auto_reserveis enabled inconfig/selective.php, the service usesBF.INSERT(which automatically reserves the filter using configured capacity and error rates if the key does not exist) rather than throwing errors. -
Bulk Operations: Implements
addMultiple(BF.MADD/BF.INSERT) andexistsMultiple(BF.MEXISTS) to enable efficient bulk seeding.
2. Validation Rules: BloomUnique & BloomExists
Instead of a hardcoded class, rules take table and column arguments dynamically.
For example, inside BloomUnique:
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$column = $this->column ?? $attribute;
$bloomKey = "{$this->table}:{$column}";
$service = app(BloomFilterService::class);
// If it might exist in the bloom filter
if ($service->exists($bloomKey, (string) $value)) {
// Fall back to database check to rule out false positives
$query = DB::table($this->table)->where($column, $value);
if ($this->ignoreId !== null) {
$query->where($this->idColumn, '!=', $this->ignoreId);
}
if ($query->exists()) {
$fail('The :attribute has already been taken.');
}
}
}
It supports .ignore($id) out of the box, making update validation simple:
new BloomUnique('users', 'email')->ignore($user->id)
3. Automatic Eloquent Synchronization: HasBloomFilters
To keep the Bloom filter updated in real-time, developers use this trait on their models and define a $bloomFilters property.
class User extends Model
{
use HasBloomFilters;
protected array $bloomFilters = ['email', 'username'];
}
The trait registers Eloquent boot hooks (created and updating). On updates, it uses isDirty($column) to only push updated values to Redis.
Note: Since Bloom filters do not support deletion, when a model is deleted from the DB, its value remains in Redis. This is fine because the database fallback handles the resulting false-positive check during validation.
4. Developer Tools: Artisan Commands
-
BloomSeedCommand: Seeds existing tables into Redis in chunks (
1000records at a time) using bulk operations (addMultiple) to initialize existing databases. -
BloomStatusCommand: Queries
BF.INFOto retrieve and format filter properties (Capacity, Size in Bytes, Items Inserted, Expansion Rate) into a neat CLI table. -
BloomClearCommand: Uses Redis
DELto reset filters manually.
Conclusion
Building Selective has been a highly rewarding journey—from a simple prototype born out of necessity to a robust, generic Laravel package. It stands as a reminder that taking the extra time to prioritize Developer Experience (DX) and thorough research can yield tools that genuinely make our lives easier.
By bridging the gap between application-layer validation and database constraints, Selective ensures your primary database isn't unnecessarily strained during high-traffic spikes. Whether you are dealing with millions of user registrations or high-volume reference checks, the combination of RedisBloom and Laravel's elegant syntax provides a seamless, fail-safe solution that scales with your application.
I encourage you to give it a spin in your next high-volume Laravel project! You can check out the full documentation and source code here:
If you find it useful, dropping a star on the repository would be greatly appreciated. I am also very open to feedback, so feel free to open issues, suggest features, or submit pull requests. Open-source tools thrive on community collaboration.
Thank you for following along with this journey. Happy coding, and may your application validations always be super-fast!
Top comments (0)