A comprehensive, searchable database of Indonesian administrative regions for Go
region-id is a production-ready Go library that provides complete access to Indonesian administrative regions (provinces, regencies, districts, and villages). Built with performance and simplicity in mind, it offers full-text search, Redis caching, and seamless integration with any Go web framework.
- 91,603 regions - Complete Indonesian administrative hierarchy
- 39 provinces • 515 regencies • 7,286 districts • 83,763 villages
- Full-text search with fuzzy matching and ranking
- Redis caching with automatic invalidation (3x faster queries)
- Auto-migration - One-command database setup
- Clean architecture - Repository pattern, domain entities, handlers
- Framework-agnostic - Works with Echo, Gin, Chi, or logistics-id/engine
- Fuzzy matching using PostgreSQL trigrams
- Smart ranking based on match quality
- Filter by type, parent, or name
- Search across name, code, and administrative area
- Redis caching with 24-hour TTL
- Singleflight pattern to prevent cache stampede
- Optimized indexes for fast queries
- Version-based cache invalidation
- 3x faster cached responses (~10ms vs ~30ms)
- Auto-migration - Run migrations programmatically
- Embedded data - No external data files needed
- Graceful degradation - Works without Redis
- Type-safe entities and repository interfaces
- Comprehensive tests - Unit tests with high coverage
- Clean architecture with separation of concerns
- Repository pattern for data access
- Domain entities with validation
- HTTP handlers with logistics-id/engine integration
- Migration system with version tracking
go get github.com/enigma-id/region-id# Using Docker Compose
docker-compose up -d postgres redis
# Or use your own PostgreSQL + Redis instancespackage main
import (
"context"
"database/sql"
"github.com/enigma-id/region-id"
"github.com/joho/godotenv"
"github.com/logistics-id/engine"
"github.com/logistics-id/engine/ds/redis"
"github.com/logistics-id/engine/transport/rest"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
"go.uber.org/zap"
)
func init() {
godotenv.Load()
engine.Init("region-id-server")
}
func main() {
engine.OnStart(func(ctx context.Context) error {
logger := engine.Logger
// Setup database
sqldb := sql.OpenDB(pgdriver.NewConnector(
pgdriver.WithDSN("postgres://postgres:postgres@localhost:5432/regiondb?sslmode=disable"),
))
db := bun.NewDB(sqldb, pgdialect.New())
// Setup Redis (optional - for caching)
cfg := &redis.Config{
Prefix: "regions",
Server: "localhost:6379",
Password: "",
}
if err := redis.NewConnection(cfg, logger); err != nil {
logger.Warn("Redis connection failed, caching disabled", zap.Error(err))
}
// Initialize with auto-migration
regionHandler, err := regionid.Initialize(regionid.Config{
DB: db,
AutoMigrate: true,
})
if err != nil {
return err
}
// Store handler for use in Run
// ... (see full example in examples/rest-server)
return nil
})
engine.Run(func(ctx context.Context) {
// Start REST server
// ... (see full example in examples/rest-server)
})
}package main
import (
"database/sql"
"github.com/enigma-id/region-id"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
)
func main() {
// Setup database
sqldb := sql.OpenDB(pgdriver.NewConnector(
pgdriver.WithDSN("postgres://postgres:postgres@localhost:5432/regiondb?sslmode=disable"),
))
db := bun.NewDB(sqldb, pgdialect.New())
// Initialize without Redis - caching disabled
regionHandler, err := regionid.Initialize(regionid.Config{
DB: db,
AutoMigrate: true,
})
if err != nil {
panic(err)
}
// Use regionHandler...
}# Run your server
go run main.go
# Test the API
curl "http://localhost:8080/regions/search?q=Jakarta&limit=5"That's it! Your database now has 91,603 Indonesian regions ready to search.
| Method | Endpoint | Description |
|---|---|---|
GET |
/regions/search |
Search regions by name with filters |
GET |
/regions/:id |
Get region by ID |
GET |
/regions/:id/children |
Get direct children of a region |
GET |
/regions/:id/path |
Get full hierarchy path |
GET /regions/search?q=Jakarta&type=province&limit=10Query Parameters:
q- Search query (region name, case-insensitive)type- Filter by type (province,regency,district,village)parent_id- Filter by parent UUIDlimit- Max results (default: 10)
Response:
{
"success": true,
"message": "success",
"data": [
{
"id": "551131ea-5323-4b5e-a8fb-a085430f01ca",
"parent_id": null,
"name": "DKI Jakarta",
"code": "31",
"type": "province",
"level": 1,
"administrative_area": {
"country": "Indonesia",
"province": "DKI Jakarta",
"country_id": "d92a0534-6dd6-47d0-a865-04d9efaacbc3",
"province_id": "551131ea-5323-4b5e-a8fb-a085430f01ca"
},
"latitude": -6.1990,
"longitude": 106.8343,
"created_at": "2025-08-19T09:05:59.85979Z",
"is_deleted": false
}
]
}GET /regions/:idReturns detailed information for a specific region.
GET /regions/:id/childrenReturns all direct children (one level down) ordered by name.
GET /regions/:id/pathReturns the full path from root (province) to the specified region:
{
"success": true,
"message": "success",
"data": [
{"name": "DKI Jakarta", "type": "province", "level": 1},
{"name": "Jakarta Selatan", "type": "regency", "level": 2},
{"name": "Tebet", "type": "district", "level": 3}
]
}config := regionid.Config{
DB: db, // Required: *bun.DB database connection
AutoMigrate: true, // Optional: Run migrations on startup (default: false)
CacheEnabled: true, // Optional: Enable Redis caching (default: true)
}Configuration Options:
| Option | Type | Default | Description |
|---|---|---|---|
DB |
*bun.DB |
Required | Database connection |
AutoMigrate |
bool |
false |
Run migrations on initialization |
CacheEnabled |
bool |
true |
Enable/disable Redis caching |
Redis Integration:
- The library uses
engine/ds/redisglobal singleton for caching - Call
redis.NewConnection()before initializing region-id to enable caching - If Redis is not initialized, the library gracefully degrades to database-only mode
- Set
CacheEnabled: falseto explicitly disable caching even if Redis is available
import "github.com/logistics-id/engine/ds/redis"
// Initialize Redis first
cfg := &redis.Config{
Prefix: "regions",
Server: "localhost:6379",
Password: "",
}
redis.NewConnection(cfg, logger)
// Then initialize region-id (caching enabled by default)
regionHandler, err := regionid.Initialize(regionid.Config{
DB: db,
AutoMigrate: true,
CacheEnabled: true, // Optional - true is default
})// Skip redis.NewConnection() and explicitly disable caching
regionHandler, err := regionid.Initialize(regionid.Config{
DB: db,
AutoMigrate: true,
CacheEnabled: false, // Disable caching
})When to disable caching:
- Debugging SQL queries
- Testing without Redis
- Development environments
- When data freshness is more important than performance
If you prefer to run migrations manually:
# Run migrations before starting your app
psql $DATABASE_URL -f pkg/migration/001_create_regions_table.up.sql
psql $DATABASE_URL -f pkg/migration/002_create_search_function.up.sql
psql $DATABASE_URL -f pkg/migration/003_create_triggers.up.sql
psql $DATABASE_URL -f pkg/migration/004_import_regions_data.up.sqlThen initialize without auto-migration:
regionHandler, err := regionid.Initialize(regionid.Config{
DB: db,
AutoMigrate: false, // Migrations already run manually
})type Region struct {
ID uuid.UUID // Unique identifier
ParentID *uuid.UUID // Parent region ID
Name string // Region name
Code string // BPS code
Type string // province, regency, district, village
Level int // Administrative level (1-4)
PostalCode string // Postal code
AdministrativeArea AdministrativeArea // Hierarchy information
Latitude *float64 // Geographic coordinates
Longitude *float64
CreatedAt time.Time
UpdatedAt time.Time
IsDeleted bool // Soft delete flag
}Administrative Levels:
- Level 1: Province (provinsi)
- Level 2: Regency/City (kabupaten/kota)
- Level 3: District (kecamatan)
- Level 4: Village (desa/kelurahan)
A complete REST API server example using logistics-id/engine is available in the examples/rest-server directory.
cd examples/rest-server
# Set environment variables
export DATABASE_URL="postgres://postgres:postgres@localhost:5432/regiondb?sslmode=disable"
export REDIS_ADDR="localhost:6379"
export SERVER_ADDR=":8080"
# Run with auto-migration
go run main.goThe example demonstrates:
- ✅ Auto-migration on startup
- ✅ Redis caching configuration
- ✅ Structured logging with Zap
- ✅ Graceful shutdown
- ✅ Health check endpoint
# Search regions
curl "http://localhost:8080/regions/search?q=Bandung&limit=3"
# Filter by type
curl "http://localhost:8080/regions/search?q=Jawa&type=province"
# Get by ID (use ID from search results)
curl "http://localhost:8080/regions/{uuid}"Benchmarks from production usage:
| Operation | Uncached | Cached | Improvement |
|---|---|---|---|
| Search query | ~30ms | ~10ms | 3x faster |
| Get by ID | ~5ms | ~2ms | 2.5x faster |
| Get children | ~15ms | ~5ms | 3x faster |
Cache Configuration:
- Search results: 24-hour TTL
- Individual regions: 24-hour TTL
- Data version: Persistent (no expiration)
- Automatic invalidation on data changes
Throughput:
- Supports 1000+ concurrent requests
- Cache hit rate: 90%+ for common queries
- Database connections managed via Bun connection pool
Complete Indonesian regions data exported and validated:
- Total: 91,603 regions
- Provinces: 39 (level 1)
- Regencies/Cities: 515 (level 2)
- Districts: 7,286 (level 3)
- Villages: 83,763 (level 4)
- Source: Official Indonesian administrative boundaries
- Export Date: January 2025
- BPS Codes: Official Indonesian statistics codes
- Validation: All entities validated before import
- Hierarchy: Complete parent-child relationships
- Geolocation: Latitude/longitude coordinates included
Data is embedded in migration files and imported using PostgreSQL COPY command:
- No staging table - Direct import for speed
- No transformation - Data matches schema exactly
- Fast import - ~4 seconds for 91,603 regions
- Embedded - No external files or downloads needed
region-id/
├── regionid.go # Main library initialization
├── pkg/
│ ├── entity/ # Domain entities
│ │ ├── region.go # Region entity with validation
│ │ └── administrative_area.go
│ ├── repository/ # Data access layer
│ │ ├── region_repository.go # Interface definition
│ │ ├── region_repository_impl.go # Implementation with caching
│ │ ├── cache_manager.go # Redis cache management
│ │ └── version_tracker.go # Data version tracking
│ ├── handler/ # HTTP handlers
│ │ ├── region_handler.go # Route registration
│ │ ├── search_request.go # Search DTO
│ │ ├── get_region_request.go # Get by ID DTO
│ │ ├── get_children_request.go # Get children DTO
│ │ └── get_path_request.go # Get path DTO
│ └── migration/ # Database migrations
│ ├── migrator.go # Migrator implementation
│ ├── 001_create_regions_table.up.sql
│ ├── 002_create_search_function.up.sql
│ ├── 003_create_triggers.up.sql
│ └── 004_import_regions_data.up.sql # Embedded 91,603 regions
└── examples/
└── rest-server/ # logistics-id/engine example
├── main.go
├── README.md
└── Dockerfile
- Repository Pattern - Clean data access abstraction
- Dependency Injection - Pass DB and Redis as dependencies
- Singleflight - Prevent cache stampede on concurrent requests
- Version-based Caching - Automatic cache invalidation
- Migration Tracking - Version-controlled schema changes
The library includes comprehensive unit tests:
# Run all tests
GOWORK=off go test ./pkg/... -v
# Run with coverage
GOWORK=off go test ./pkg/... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out- ✅ Entity validation tests
- ✅ Repository interface tests
- ✅ Migration system tests
- ✅ Cache manager tests
See TESTING.md for details.
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: regiondb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- ./pkg/migration:/docker-entrypoint-initdb.d:ro
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lrudocker-compose up -dFROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
FROM alpine:latest
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]Contributions are welcome! Please feel free to submit a Pull Request.
# Clone the repository
git clone https://github.com/enigma-id/region-id.git
cd region-id
# Run tests
GOWORK=off go test ./pkg/...
# Run example
cd examples/rest-server
go mod tidy
go run main.go- Follow Go conventions and effective Go guidelines
- Write tests for new features
- Update documentation for API changes
- Use meaningful commit messages
MIT License - see LICENSE for details.
- Indonesian Government - For the official administrative region data
- BPS (Badan Pusat Statistik) - For the region codes and standards
- logistics-id/engine - For the excellent REST framework
Built with ❤️ for the Go community