Runtime Formula Evaluation With MVEL Library in Spring Boot
Learn how to use MVEL in Spring Boot to move business rules into the database, reduce deployments, and support dynamic runtime calculations.
Join the DZone community and get the full member experience.
Join For FreeIn our software development processes, business units constantly want to update discount rates, loyalty points, or salary calculation logic.
If this logic is within the code, between when-or-if-else blocks, every change means a new unit test process, code analysis, CI/CD pipeline work, and ultimately a "deployment."
In this article, we will separate the business logic from the code, making it manageable in the database and reliably interpretable at runtime. By increasing flexibility, we will ensure the system's stable operation continues without interruption.
To do all this, we will examine how to use the MVEL (MVFLEX Expression Language) library below.
The Cost of Static Code: Why Should We Avoid It?
Generally, point calculations are as follows:
fun calculatePoints(pointType: String, factor: Int): Long {
return when (pointType) {
"INITIAL" -> 100L
"BIRTHDAY" -> 50L
"TENURE_5_10" -> factor * 10L
"TENURE_10_20" -> factor * 20L
"TENURE_20_PLUS" -> factor * 30L
else -> 0L
}
}
When looking at the code, what appears is more of a maintenance burden than a simple function. If the factors change or a new rule is added, the code is triggered from the beginning.
However, these values are actually data, not code.
Architectural Approach
Below, you will find how it works when we add the Formula engine.
import org.mvel2.MVEL
val formula = "factor * 20"
val vars = mapOf("factor" to 5)
val result = MVEL.eval(formula, vars)
In this architecture, the code does not know "how to calculate"; It only knows how to call the 'Formula engine.'
Database Design
Converting Rules to Data
We can store business rules in a flexible table. This ensures manageability.
CREATE TABLE t_point_type (
point_type_id NUMBER PRIMARY KEY,
point_type_name VARCHAR2(100),
point_formula VARCHAR2(500),
description VARCHAR2(1000)
);
Sample data:
| point_type_id | point_type_name | point_formula |
|:-------------:|:---------------:|:-----------------------:|
| 1 | INITIAL | `100` |
| 2 | BIRTHDAY | `50` |
| 3 | TENURE_5_10 | `factor * 10` |
| 4 | TENURE_10_20 | `factor * 20` |
| 5 | TENURE_20_PLUS | `factor * 30` |
| 6 | PROMOTIONAL | `factor * multiplier` |
Application Layer
The most critical point to consider in MVEL integration is performance and error management.
1. Entity Definition
@Entity
@Table(name = "t_point_type")
data class PointTypeEntity(
@Id
@Column(name = "point_type_id")
val pointTypeId: Long? = null,
@Column(name = "point_type_name")
val pointTypeName: String? = null,
@Column(name = "point_formula")
val pointFormula: String? = null
)
2. MvelUtil: Performance-Oriented Helper Class
Considering the CPU cost of parsing strings in every request, we should use compiled expressions and caching mechanisms.
@Component
class MvelUtil {
fun evaluateFormula(formula: String, factor: Int): Long {
return try {
val variables = mapOf("factor" to factor)
val result = MVEL.eval(formula, variables)
when (result) {
is Number -> result.toLong()
else -> 0L
}
} catch (e: Exception) {
throw BusinessException(
errorCode = ErrorCodes.MVEL_FORMULA_EVALUATION_FAILED,
errorDesc = "Formula evaluation failed: $formula, factor: $factor — ${e.message}"
)
}
}
fun evaluateFormulaAsString(formula: String, factor: Int): String {
return try {
val variables = mapOf("factor" to factor)
MVEL.eval(formula, variables).toString()
} catch (e: Exception) {
throw BusinessException(
errorCode = ErrorCodes.MVEL_FORMULA_EVALUATION_FAILED,
errorDesc = "Formula evaluation failed.: $formula — ${e.message}"
)
}
}
}
3. Service Layer and Business Logic
Therefore, our service layer simply receives the data and triggers the formula engine.
@Service
class PointCalculationService(
private val pointTypeRepository: PointTypeRepository,
private val mvelUtil: MvelUtil
) {
fun calculatePoints(pointTypeId: Long, factor: Int): Long {
val pointType = pointTypeRepository.findById(pointTypeId)
.orElseThrow { BusinessException(ErrorCodes.POINT_TYPE_NOT_FOUND) }
val formula = pointType.pointFormula
?: throw BusinessException(ErrorCodes.POINT_FORMULA_NOT_DEFINED)
val points = mvelUtil.evaluateFormula(formula, factor)
if (points <= 0) {
log.info("The formula gave a score of 0 or negative: type=$pointTypeId, factor=$factor")
return 0L
}
return points
}
}
Call service:
val factor = inputData.factorSpecificForPoint ?: 1
val points = calculatePoints(inputData.pointTypeId, factor)
if (points > 0) {
savePointDetail(points, subscriptionId, inputData.pointTypeId, inputData.operationId)
}
Advanced Usage: Multivariable and Conditional Formulas
MVEL has the ability to decode complex strings. Its true power lies in this.
For example, the formula in the database might look like this:
UPDATE t_point_type
SET point_formula = 'factor * multiplier + bonus'
WHERE point_type_id = 6;
fun evaluateWithMultipleVars(formula: String, vars: Map<String, Any>): Long {
return try {
val result = MVEL.eval(formula, vars)
(result as? Number)?.toLong() ?: 0L
} catch (e: Exception) {
throw BusinessException(ErrorCodes.MVEL_FORMULA_EVALUATION_FAILED)
}
}
val vars = mapOf("factor" to 5, "multiplier" to 3, "bonus" to 10)
evaluateWithMultipleVars("factor * multiplier + bonus", vars)
Conditional Statements
MVEL supports ternary expressions and Boolean logic:
factor > 10 ? factor * 20 : factor * 10
(factor >= 5 && factor < 10) ? 50 : (factor >= 10 ? 100 : 25)
This provides truly dynamic rules without any code changes.
We must not ignore these three rules, as everything is necessary;
- Strict validation: The formula must be validated with MVEL.compileExpression() before being saved to the database. An incorrect syntax error can disrupt the entire flow at runtime.
- Sandbox and security: MVEL is robust; it can access Java classes. Therefore, formula entry should only be done from authorized (admin) panels, and if necessary, MVEL's secure mode should be configured.
- Default value: There can always be a fallback mechanism. We determine how the system will behave if the formula receives an error or the result returns null (e.g., 0 points).
Conclusion
MVEL makes it easy for us to dynamically implement business rules in Spring Boot projects.
It reduces code complexity while allowing you to respond to business unit requests within minutes (without deployment!).
Dependency (Maven):
XML
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.5.0.Final</version>
</dependency>
Opinions expressed by DZone contributors are their own.
Comments