← All posts
SQL for Elasticsearch · Part 1

Stop Rewriting Your Elasticsearch Code Every Version Upgrade

One client API across Elasticsearch 6, 7, 8 and 9 — so a major-version upgrade stops meaning a rewrite.

·8 min read

The Version Upgrade Nightmare 😤

Picture this: It’s Monday morning. Your DevOps team just announced that production Elasticsearch clusters are being upgraded from version 7 to 8. Great news for security and performance, right?

Not so fast.

You open your IDE and realize that 40% of your codebase needs to be rewritten. The RestHighLevelClient you’ve been using? Deprecated. Your carefully crafted queries? Breaking changes. Your bulk operations? Different API.

Welcome to the Elasticsearch version upgrade treadmill — where every major version means days (or weeks) of refactoring, testing, and praying nothing breaks in production.

Sound familiar?

You’re not alone.

The True Cost of Version Lock-In 💸

Let’s talk numbers. When your codebase is tightly coupled to a specific Elasticsearch version, here’s what you’re really paying:

1. Developer Time = Money

  • Average refactoring time per service: 3–5 days
  • Number of microservices using ES: 10–20+
  • Total cost: Weeks of engineering time that could be spent building features

2. Technical Debt Accumulation

  • Teams delay upgrades because “it’s too much work”
  • Security patches get skipped
  • Performance improvements remain out of reach
  • The gap between your version and the latest grows wider

3. Production Risk

  • Rushed migrations lead to bugs
  • Incomplete testing causes downtime
  • Rollback complexity increases exponentially
  • Customer trust erodes with every incident

4. Team Morale

Let’s be honest — no engineer joined your company to rewrite the same Elasticsearch queries every 18 months. They want to solve real problems, not fight with API changes.

Why Does This Keep Happening? 🤔

Elasticsearch’s official clients are version-specific by design:

// Elasticsearch 6 & 7: RestHighLevelClient
val client = new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost", 9200, "http"))
)
// Elasticsearch 8+: New Java API Client
val client = new ElasticsearchClient(
new RestClientTransport(restClient, new JacksonJsonpMapper())
)

Different classes. Different APIs. Different mental models.

Every major version brings:

  • ✅ Better features
  • ✅ Performance improvements
  • Breaking changes that cascade through your entire codebase

The Elasticsearch team has valid reasons for these changes — but you shouldn’t have to pay the migration tax every time.

The Solution: Write Once, Run Everywhere ✨

What if your Elasticsearch code looked like this instead?

import app.softnetwork.elastic.client._
// Works on ES 6, 7, 8, AND 9
val client: ElasticClientApi = ElasticClientFactory.create()
// Index a document
client.index(
index = "users",
id = "user-123",
document = """{"name":"Alice","email":"alice@example.com","age":30}""",
wait = true
)
// Search using SQL (yes, SQL!)
val results = client.search(
SQLQuery("SELECT * FROM users WHERE age > 25 ORDER BY name")
)
// Bulk operations with backpressure
implicit val bulkOptions = BulkOptions(defaultIndex = "products")
client.bulkSource(
items = productStream,
toDocument = product => product.toJson
).runWith(Sink.foreach {
case Right(success) => logger.info(s"✅ Indexed: ${success.id}")
case Left(failure) => logger.error(s"❌ Failed: ${failure.error}")
})

Same code. Four Elasticsearch versions. Zero refactoring.

Meet SoftClient4ES: Your Version-Agnostic Elasticsearch Client 🚀

SoftClient4ES is a production-ready Scala client that abstracts away version differences behind a unified, stable API.

Core Philosophy

  1. Version Resilience: Write your code once, deploy it against any supported ES version
  2. Type Safety: Leverage Scala’s type system for compile-time guarantees
  3. Production Ready: Built-in error handling, validation, and monitoring
  4. Developer Experience: Intuitive APIs that feel natural to Scala developers

How It Works: The Magic Behind Version Agnosticism 🎩

SoftClient4ES uses a trait-based abstraction layer with multiple backend implementations:

// Unified API trait
trait ElasticClientApi
extends SearchApi
with IndexApi
with BulkApi
with MappingApi
// ... 12+ composable APIs
// Backend implementations
class JavaClientApi // For ES 8 & 9
class RestHighLevelClientApi // For ES 6 & 7
class JestClientApi // For ES 6 (alternative)

At runtime, the Service Provider Interface (SPI) automatically loads the client available in your classpath:

// Create a unified client using Service Provider Interface (SPI)
val client = ElasticClientFactory.create()

Your code never changes. Only the underlying implementation adapts.

Real-World Example: Before & After 📊

Before: Version-Specific Nightmare

// Elasticsearch 7 code
import org.elasticsearch.client.RestHighLevelClient
import org.elasticsearch.action.search.SearchRequest
import org.elasticsearch.index.query.QueryBuilders
val request = new SearchRequest("users")
request.source().query(
QueryBuilders.boolQuery()
.must(QueryBuilders.rangeQuery("age").gt(25))
)
val response = client.search(request, RequestOptions.DEFAULT)
// ❌ Breaks on ES 8 - RestHighLevelClient deprecated
// ❌ Verbose boilerplate
// ❌ No type safety
// ❌ Manual error handling

After: Version-Agnostic Bliss

import app.softnetwork.elastic.client._
val results = client.searchAs[User](
"SELECT name, email FROM users WHERE age > 25"
)
// ✅ Works on ES 6, 7, 8, 9
// ✅ Clean, readable SQL
// ✅ Type-safe results
// ✅ Built-in error handling

Same functionality. 90% less code. Infinite version compatibility.

Beyond Version Agnosticism: Killer Features 🔥

1. SQL to Elasticsearch Translation

// Complex query in plain SQL
val query = """
SELECT
category,
AVG(price) as avg_price,
COUNT(*) as product_count
FROM products
WHERE stock > 0
AND price BETWEEN 10 AND 100
AND description LIKE '%laptop%'
GROUP BY category
HAVING avg_price > 50
ORDER BY product_count DESC
LIMIT 10
"""
client.search(SQLQuery(query))

Automatically translated to optimized Elasticsearch DSL with aggregations.

2. Zero-Downtime Mapping Migrations

val newMapping = """{
"properties": {
"email": {"type": "keyword"}, // Changed from "text"
"age": {"type": "integer"},
"verified": {"type": "boolean"} // New field
}
}"""
// Automatic migration with rollback on failure
client.updateMapping("users", newMapping) match {
case ElasticSuccess(_) =>
logger.info("✅ Migration completed successfully")
case ElasticFailure(error) =>
logger.error(s"❌ Migration failed: ${error.message}")
// Original mapping automatically restored
}

Behind the scenes:

  1. Backup current mapping
  2. Create temporary index with new mapping
  3. Reindex all data
  4. Atomic swap via aliases
  5. Automatic rollback if any step fails

3. Reactive Streams for Bulk Operations

// Process millions of documents without memory issues
val obsoleteProducts: Source[(Product, ScrollMetrics), NotUsed] =
client.scrollAs[Product](
"SELECT id, name FROM products WHERE obsolete = true",
client.defaultScrollConfig
)
implicit val bulkOptions = BulkOptions(
defaultIndex = "products",
maxBulkSize = 1000,
balance = 4 // Parallelism
)
obsoleteProducts
.map { case (product, _) => s"""{"id": "${product.id}"}""" }
.via(client.bulkFlow(delete = true))
.runWith(Sink.foreach {
case Right(success) => metrics.increment("deleted")
case Left(failure) => logger.error(s"Failed: ${failure.error}")
})

Backpressure-aware, memory-efficient, production-tested.

Production-Ready from Day One 🛡️

Built-In Monitoring

val client = ElasticClientFactory.createWithMonitoring()
// Automatic metrics every 30 seconds:
// === Elasticsearch Metrics ===
// Total Operations: 1,247
// Success Rate: 98.5%
// Average Latency: 45ms
// P95 Latency: 120ms
// =============================
// Automatic alerts:
// ⚠️ HIGH FAILURE RATE: 15.0%
// ⚠️ HIGH LATENCY: 1200ms

Comprehensive Error Handling

sealed trait ElasticResult[+T] {
def isSuccess: Boolean
def get: T
def getOrElse[B >: T](default: => B): B
def error: Option[ElasticError]
def map[B](f: T => B): ElasticResult[B]
def flatMap[B](f: T => ElasticResult[B]): ElasticResult[B]
def toOption: Option[T]
def toEither: Either[ElasticError, T]
def fold[B](onFailure: ElasticError => B, onSuccess: T => B): B
def foreach(f: T => Unit): Unit
}
case class ElasticSuccess[T](value: T) extends ElasticResult[T]
case class ElasticFailure[T](error: ElasticError) extends ElasticResult[T]
case class ElasticError(
message: String,
cause: Option[Throwable] = None,
statusCode: Option[Int] = None,
index: Option[String] = None,
operation: Option[String] = None
)

Functional error handling. No exceptions in production.

Getting Started in 5 Minutes ⏱️

1. Add Dependency

build.sbt
ThisBuild / resolvers +=
"Softnetwork Server" at "https://softnetwork.jfrog.io/artifactory/releases/"
// Choose your Elasticsearch version
libraryDependencies += "app.softnetwork.elastic" %% "softclient4es8-java-client" % "0.21.0"

2. Initialize Client

import app.softnetwork.elastic.client._
val client = ElasticClientFactory.create()

3. Start Building

// Create index
val mapping = """{
"properties": {
"name": {"type": "text"},
"email": {"type": "keyword"}
}
}"""
client.createIndex("users", mapping)
// Index documents
client.index(
"users",
"user-1",
"""{"name":"Alice","email":"alice@example.com"}""",
wait = true
)
// Search
val results = client.searchAs[User](
"SELECT name, email FROM users WHERE name = 'Alice'"
)

That’s it. No version-specific configuration. No complex setup.

The Migration Path: From Official Clients to SoftClient4ES 🛤️

Phase 1: Parallel Adoption (Week 1)

  • Add SoftClient4ES alongside existing client
  • Migrate read-only operations first (searches, gets)
  • Run both clients in parallel, compare results

Phase 2: Write Operations (Week 2)

  • Migrate indexing and updates
  • Test bulk operations thoroughly
  • Monitor error rates and latency

Phase 3: Advanced Features (Week 3–4)

  • Implement SQL queries where appropriate
  • Set up automatic mapping migrations
  • Enable monitoring and alerting

Phase 4: Cleanup (Week 5)

  • Remove old client dependencies
  • Delete version-specific code
  • Document the new patterns

Total migration time: 4–5 weeks for a typical microservice.

Who Should Use SoftClient4ES? 🎯

SoftClient4ES is designed for teams and projects that face specific Elasticsearch challenges. Here’s who benefits most:

1. Multi-Version Environments 🔄

You should use SoftClient4ES if:

  • You maintain multiple microservices running different ES versions
  • You’re planning a gradual migration from ES 6/7 to 8/9
  • You support multiple clients with different ES infrastructure
  • You want to future-proof your codebase against version changes

Example scenario:

“We have 15 microservices: 5 on ES 6, 7 on ES 7, and 3 on ES 8. Maintaining version-specific code was a nightmare. SoftClient4ES gave us a unified codebase.”

2. Teams That Value Developer Experience 👨‍💻

You should use SoftClient4ES if:

  • You prefer SQL to complex JSON DSL
  • You want compile-time type safety over runtime errors
  • You value clean, maintainable code
  • Your team includes developers new to Elasticsearch

Example scenario:

“Our backend developers know SQL inside-out but struggle with Elasticsearch DSL. SoftClient4ES’s SQL support reduced onboarding time from weeks to days.”

3. Production-Critical Applications 🛡️

You should use SoftClient4ES if:

  • Downtime during schema changes is not acceptable
  • You need automatic rollback mechanisms
  • Monitoring and observability are essential
  • You require robust error handling and validation

Example scenario:

“We handle 10M+ documents daily. Zero-downtime migrations and built-in monitoring are non-negotiable for us. SoftClient4ES delivers both.”

4. High-Volume Data Processing 📊

You should use SoftClient4ES if:

  • You process millions of documents regularly
  • Memory efficiency matters
  • You need backpressure-aware bulk operations
  • You’re already using Akka Streams or want to

Example scenario:

“We ingest 50GB of log data daily. SoftClient4ES’s streaming bulk API with Akka Streams handles our volume without breaking a sweat.”

5. Event-Sourced Systems 🎭

You should use SoftClient4ES if:

  • You use Akka Persistence or similar event-sourcing frameworks
  • You need to project events to Elasticsearch
  • You want seamless integration between your domain model and search
  • You follow CQRS patterns

Example scenario:

“Our event-sourced microservices needed a clean way to project domain events to Elasticsearch. The built-in Akka Persistence integration was exactly what we needed.”

6. Scala-First Organizations 🔧

You should use SoftClient4ES if:

  • Your stack is Scala-based (Play, Akka, ZIO, Cats Effect)
  • You value functional programming principles
  • You want idiomatic Scala APIs, not Java wrappers
  • Type safety is a core requirement

Example scenario:

“We’re a Scala shop. The official Java client works, but it doesn’t feel natural. SoftClient4ES’s Scala-first design fits perfectly with our codebase.”

When NOT to Use SoftClient4ES ⚠️

SoftClient4ES might NOT be the best fit if:

You’re using JavaScript/Python/Go → SoftClient4ES is Scala-only. Use official clients for other languages.

You need bleeding-edge ES features immediately → Official clients get new features first. We follow with stable implementations.

Your team has zero Scala experience → Learning Scala + SoftClient4ES simultaneously has a steep curve.

You’re building a simple prototype → Official clients might be faster to set up for throwaway code.

You only use basic CRUD operations → SoftClient4ES shines with complex queries, migrations, and streaming. Simple use cases might not justify the dependency.

The Sweet Spot 🎯

SoftClient4ES is perfect for:

Medium to large Scala teams (5+ engineers) ✅ Production systems with high uptime requirementsProjects with 2+ year lifespans (where version migrations are inevitable) ✅ Data-intensive applications (millions of documents) ✅ Teams that value maintainability over quick hacks

Still Not Sure? 🤔

Ask yourself these questions:

  1. Have I rewritten Elasticsearch code due to version upgrades? → If yes, you’ll love version agnosticism
  2. Do I spend more time debugging JSON queries than solving business problems? → If yes, SQL support will save you hours
  3. Have I ever caused downtime during a mapping change? → If yes, zero-downtime migrations are for you
  4. Do I process large datasets regularly? → If yes, streaming APIs are essential
  5. Am I building something meant to last? → If yes, invest in maintainable abstractions

If you answered “yes” to 2+ questions, give SoftClient4ES a try.

Try It Risk-Free 🚀

The SoftClient4ES core engine is open source (Apache License 2.0) — the SQL engine, the client API and the REPL. The extensions, drivers, sidecar and federation server ship under the Elastic License 2.0: free to use, sources not public. You can:

Evaluate it without vendor lock-in ✅ Contribute if you find issues or need features ✅ Fork it if your needs diverge ✅ Learn from it even if you don’t adopt it

Start with a small, non-critical service. See if it fits your workflow. Scale from there.

Get Started:

Does your use case fit? What challenges are you facing with Elasticsearch? Let’s discuss in GitHub Discussions.

Open Source & Community-Driven 💪

The SoftClient4ES core is open source (Apache License 2.0) because this problem affects everyone.

Contribute:

  • 🐛 Report issues: GitHub Issues
  • 💬 Join discussions: GitHub Discussions
  • 🔧 Submit PRs: We welcome contributions!
  • Star the repo: Help others discover the project

Roadmap 🗺️

  • Q3 2025: ES 6–9 support, SQL SELECT, Compile-Time SQL Query Validation
  • Q4 2025: SQL DML (CREATE/ALTER/INSERT/UPDATE/DELETE) — shipped
  • Q1 2026: Full JDBC connector — shipped, along with ADBC, an Arrow Flight SQL server, and SQL-defined materialized views
  • Q3 2026: Cross-index and cross-cluster JOIN, on every client surface

FAQ ❓

Q: Can I mix official clients and SoftClient4ES?

A: Yes! You can adopt it gradually, service by service.

Q: What about performance overhead?

A: Minimal. The abstraction layer adds <5ms latency.

The Bottom Line 💡

Stop paying the version upgrade tax.

Every hour spent rewriting Elasticsearch queries is an hour NOT spent:

  • Building features your customers actually want
  • Improving system performance
  • Solving interesting technical challenges
  • Growing as an engineer

SoftClient4ES gives you back that time.

Write your Elasticsearch code once. Deploy it everywhere. Focus on what matters.

Ready to Break Free from Version Lock-In? 🚀

// Your future Elasticsearch code
val client = ElasticClientFactory.create()
// Works today. Works tomorrow. Works on ES 6, 7, 8, 9.
client.search(SQLQuery("SELECT count(*) FROM your_data WHERE magic = true"))

Get Started:

What’s your biggest Elasticsearch version upgrade horror story? Share it in GitHub Discussions — I’d love to hear how other teams are handling this.

Built with ❤️ by the SoftNetwork team

This post first appeared on Medium. This is the maintained version — free to read, no account, and corrected as the product moves.

Try it on your own cluster

SoftClient4ES runs SQL — DDL, DML, queries, cross-index JOINs and materialized views — on Elasticsearch 6 through 9.