BadgerHold puts a query and index layer over a Badger key value store
BadgerHold is an embeddable NoSQL store for querying Go types built on Badger
At a glance
- What is it?
- BadgerHold is a Go library that stores Go structs in a Badger key value database and gives you chainable queries, struct tag driven indexes and bulk delete and update over matching records, while leaving the underlying Badger instance open for anything the layer does not cover. It suits a Go service that wants a document style API without running a separate database server.
- Who is it for?
- Adopt BadgerHold if you are writing a Go service that wants to persist and query Go structs through an embedded key value store, because the library removes the need to hand-write serialisation and index maintenance while still letting you reach into Badger when the layer is not enough.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 114 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A querying layer over Badger, with the store left open
BadgerHold is described as a simple querying and indexing layer on top of a Badger instance, and the goal is stated with unusual clarity: to create a simple, higher level interface on top of Badger DB that simplifies dealing with Go types and finding data, but exposes the underlying Badger DB for customizing as you wish. That last clause is the design decision that matters. The library is not a walled garden, it is a convenience layer with an escape hatch, which is why it suits a Go developer who wants the ergonomics of a document store without giving up direct access to the key value engine. Badger itself is the embedded, pure Go key value store from dgraph-io, which means the whole database lives inside your process and there is no server to run. The project is MIT licensed, is not archived, and the last push to the repository was on 2026-07-02. There are no GitHub releases, so the version is tracked in go.mod, where the module is declared as github.com/timshannon/badgerhold/v4 on Go 1.13, with dgraph-io/badger v4.1.0 as the direct dependency. The repository is flat and readable, with one file per operation such as put.go, get.go, delete.go, find_test.go, index.go, aggregate.go, compare.go and store.go, which is what you would expect from a library whose API surface is the point.
Adding BadgerHold to a Go module
There is no installation command in the README, so the practical route is the standard Go module path, which the repository's go.mod states as github.com/timshannon/badgerhold/v4. Note the /v4 suffix, since that is the major version segment and an import without it will not resolve to this module. The same file pins the language version at go 1.13 and the direct dependency on dgraph-io/badger v4.1.0, so the Badger version is chosen for you rather than negotiated. The first thing to do after the dependency is in place is to open a store, which is where the Options struct comes in: if you want to swap Gob for your own encoding, this is where you pass your encode and decode functions. From there the shape of a program is fixed by the API: a store, a struct with exported fields, a Put to write a record, and a Find with a Where clause to read it back. The repository ships example_test.go, which is where that sequence is written out as a runnable example, and the rest of the test files show the same store opened with different Options.
Encoding: Gob by default, anything you like if you insist
The default encoding is Gob, and the README notes that you can feel free to use the GobEncoder and GobDecoder interface for faster serialization. Or you can use any serialization you want by supplying encode and decode funcs to the Options struct on Open. This is the second thing worth understanding about the library, because it defines the cost of storing a record. Gob is Go's own encoding and it is fast for Go to Go, but it is tied to Go structs and it is not a stable storage format across language versions in the way a document format is. If you need to read the database from something other than Go, or you need a format you control, the escape hatch is the Options struct, where you plug in your own encode and decode functions. The other structural rule is type separation. One Go Type will be prefixed with its type name, so you can store multiple types in a single Badger database without conflicts, which is the mechanism behind the sample-type arguments you see in the query calls later on. The project is also a rewrite of the earlier BoltHold project, moving from Bolt to Badger, and the README links both a performance comparison from the Badger authors and the author's own write-up focused on characteristics other than performance.
Indexes: skipping records, at the cost of extra disk work
Indexes are the performance lever and the trade-off is explicit. Indexes allow you to skip checking any records that don't meet your index criteria, and the README gives the arithmetic: if you have 1000 records and only 10 of them are of the Division you want, you do not need to check the other 990 against your query criteria if you create an index on the Division field. The downside is added disk reads and writes on every write operation, and for read heavy datasets indexes can be very useful. So an index is a write tax paid for read speed, and the README is upfront that it applies on every write. Mechanically, every store has a reserved bucket called _indexes that holds indexes pointing back to another bucket's key system, and indexes are defined by setting the badgerhold index struct tag on a field.
type Person struct {
Name string
Division string `badgerhold:"index"`
}
// alternate struct tag if you wish to specify the index name
type Person struct {
Name string
Division string `badgerholdIndex:"IdxDivision"`
}The index created for Division contains the set of unique divisions and the main record keys they refer to. If you want finer control you can implement the Storer interface to specify your own indexes instead of using the struct tag.
The query chain and its operator set
Queries are chain-able constructs that filter out any data that doesn't match their criteria, and the important detail about indexes is here: an index will be used if the Index chain is called, otherwise BadgerHold won't use any index. That means a query without an explicit Index call is a scan, so the struct tag alone does not make your queries fast.
s.Find(&result, badgerhold.Where("FieldName").Eq(value).And("AnotherField").Lt(AnotherValue).Or(badgerhold.Where("FieldName").Eq(anotherValue)))Fields must be exported, and thus always need to start with an upper-case letter. The operator list is long enough to cover most filtering: Eq, Ne, Gt, Lt, Le, Ge for comparisons, In for membership, IsNil, RegExp for a regular expression, MatchFunc which takes a function over a RecordAccess and returns a bool and an error, Skip, Limit, SortBy across several fields, Reverse, Index by index name, Contains, ContainsAll, ContainsAny for collection membership, and HasKey to test whether a Map value has a key. An empty or zero value query matches against all records, because it has no criteria, and you can page through a dataset with Skip and Limit.
q := &badgerhold.Query{}
err := store.Find(&result, q.Skip(10).Limit(50))You can also target the key itself with the badgerhold.Key constant, and you can reach nested struct fields with a dotted path such as Contact.Name.
Comparing fields against each other
One of the more capable parts of the API is comparing a field against another field in the same struct rather than against a constant. The README's example is a Person with Name, Birth and Death time fields, and the goal is to find invalid records where a death came before a birth. That is expressed with the Field helper.
store.Find(&result, badgerhold.Where("Death").Lt(badgerhold.Field("Birth")))This is the kind of check that a plain key value store cannot express at all, and it is exactly the sort of data integrity query that shows up when you have migrated records from somewhere else. Nested structures work the same way, with the field path written as Contact.Name for a Repo struct holding a Contact ContactPerson field. There is a subtlety worth noting for anyone reading the README code: the nested example in the documentation is missing a closing parenthesis on the Find call, so treat the README snippets as illustrations rather than something to paste unchanged.
Bulk delete and bulk update over a query
Queries are not limited to selecting data. You can delete or update data that matches a query, and the README shows both. To remove the invalid records where Death is before Birth, you pass a sample type so BadgerHold knows which bucket to use and what indexes to update.
// you must pass in a sample type, so BadgerHold knows which bucket to use and what indexes to update
store.DeleteMatching(&Person{}, badgerhold.Where("Death").Lt(badgerhold.Field("Birth")))To flip the Birth and Death dates on the same set of records, UpdateMatching takes a callback. The record passed to the callback is always a pointer, so the type assertion is checked and an error returned if it does not match.
store.UpdateMatching(&Person{}, badgerhold.Where("Death").Lt(badgerhold.Field("Birth")), func(record interface{}) error {
update, ok := record.(*Person) // record will always be a pointer
if !ok {
return fmt.Errorf("Record isn't the correct type! Wanted Person, got %T", record)
}
update.Birth, update.Death = update.Death, update.Birth
return nil
})There is also a Count method for the number of records a query returns, which takes an empty data type for the same reason.
// need to pass in empty datatype so badgerhold knows what type to count
count, err := store.Count(&Person{}, badgerhold.Where("Death").Lt(badgerhold.Field("Birth")))For a single record, FindOne is a shorthand for Find with Limit(1) and returns an ErrNotFound when nothing matches, unlike a normal Find where an empty slice is returned.
Where BadgerHold is the wrong tool
The boundaries follow from Badger itself. First, this is a single process store. Badger is an embedded database, so there is one database per process, and a service that scales horizontally by running several instances needs each instance to have its own data or a different design, which rules BadgerHold out for shared multi writer workloads. Second, the query language is a fixed operator set over Go struct fields, and there is no aggregation pipeline beyond the count and aggregate files in the tree, no text search, no joins across types and no transaction spanning several records, so a workload that needs any of those is looking at a different category of tool. Third, the index write tax is real and the README says so plainly, so a write heavy workload with indexes is paying for reads it may never issue. Fourth, Gob as the default encoding ties your stored data to Go types, which is fine while the service is a Go service and is a liability the moment you want to read the data elsewhere or survive a major refactor of your structs, and the escape hatch through the Options struct requires you to write and maintain those functions. Fifth, the maintenance signal is mixed: the repository is not archived and was pushed to on 2026-07-02, but there are no GitHub releases at all, so nothing tells you which commit is the one to pin, and the go.mod still declares Go 1.13 while depending on Badger v4. The honest comparison is with BoltHold, the Bolt based predecessor this project was rewritten from, which has the same API shape over a different engine, and with Badger's own transaction and iterator API for code that only needs ordered iteration or atomic multi key updates and does not need a document style query layer.
Editorial conclusion
Adopt BadgerHold if you are writing a Go service that wants to persist and query Go structs through an embedded key value store, because the library removes the need to hand-write serialisation and index maintenance while still letting you reach into Badger when the layer is not enough. Do not adopt it if you need multi machine durability, transactions across records, or a query language richer than the listed operators, since a single embedded Badger database is a single-process store. Verify two things before you commit: that your Go types use exported fields only, because the README states query fields must be exported and start with an upper-case letter, and that your module path matches the major version, since the go.mod declares module github.com/timshannon/badgerhold/v4. The practical first step is to open a store with an Options value, put one struct in it and run a Find with a Where clause to see the round trip before you design your index strategy.
Frequently asked questions
What is BadgerHold?
It is a simple querying and indexing layer on top of a Badger instance, giving Go types a higher level interface for storing and finding data while still exposing the underlying Badger database for customisation. It is MIT licensed and the module path is github.com/timshannon/badgerhold/v4.
How do I create an index in BadgerHold?
Set the badgerhold index struct tag on the field you want indexed, for example Division string `badgerhold:"index"`, or use the badgerholdIndex tag with an explicit name such as `badgerholdIndex:"IdxDivision"`. You can also implement the Storer interface to define your own indexes instead of using the struct tag.
Which query operators does BadgerHold support?
The documented set covers Eq, Ne, Gt, Lt, Le, Ge, In, IsNil, RegExp, MatchFunc, Skip, Limit, SortBy, Reverse, Index, Contains, ContainsAll, ContainsAny and HasKey. Fields must be exported, so they always start with an upper-case letter, and an index is used only when the Index chain is called.
How do I compare one field against another in a BadgerHold query?
Use the badgerhold.Field helper instead of a literal value, so a query such as badgerhold.Where("Death").Lt(badgerhold.Field("Birth")) finds records whose Death value is less than their Birth value. The same applies to nested fields, written with a dotted path such as Contact.Name.
Can I delete or update every record matching a query in BadgerHold?
Yes. DeleteMatching takes a sample type so the library knows which bucket and indexes to touch, and UpdateMatching takes a callback that receives each matching record as a pointer and returns an error. There is also a Count method that returns the number of records a query matches.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/timshannon-badgerhold)Was this useful?