Definition
- Postfix (Reverse Polish Notation) – operators come after their operands, e.g.
3 4 + 2 *means(3 + 4) * 2. - Stack evaluation – scan tokens left to right, push numbers, and on each operator pop two operands, apply the operation, and push the result.
- Operand order – the first pop is the right operand (b), the second is the left (a); compute
a op b. - No precedence – the token order already encodes the evaluation order, so no parentheses or precedence rules are needed.
- Banking – use
BigDecimalwith explicit scale andRoundingModefor exact money arithmetic. - Validation – a well-formed expression leaves exactly one value on the stack.
Rajesh computes a fee formula written in a special order where the amounts come first and the operation comes last, like "base tax add rate multiply." He keeps a tray of numbers. Every time he reads a number, he drops it on the tray. When he reads an operation like "add," he takes the top two numbers off the tray, adds them, and puts the result back. At the end there is exactly one number on the tray, and that is the final fee.
Understanding the Concepts
What is postfix evaluation?
Postfix notation, also called Reverse Polish Notation (RPN), places operators after their operands. The expression 3 4 + 2 * means add 3 and 4 first, then multiply by 2, giving 14. Because the operator order in the string already encodes the evaluation order, there are no parentheses and no precedence rules.
package com.bank.service;
import java.util.ArrayDeque;
import java.util.Deque;
public class PostfixEvaluator {
public static int evaluate(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : tokens) {
switch (token) {
case "+": { int b = stack.pop(); int a = stack.pop(); stack.push(a + b); break; }
case "-": { int b = stack.pop(); int a = stack.pop(); stack.push(a - b); break; }
case "*": { int b = stack.pop(); int a = stack.pop(); stack.push(a * b); break; }
case "/": { int b = stack.pop(); int a = stack.pop(); stack.push(a / b); break; }
default: stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}
public static void main(String[] args) {
// (3 + 4) * 2 = 14
String[] expr = {"3", "4", "+", "2", "*"};
System.out.println(evaluate(expr)); // 14
}
}
14
Why does the stack make this work?
Each operator needs exactly two operands. The stack holds operands waiting for their operator. When an operator arrives, the two most recent operands are on top of the stack. Popping them, computing, and pushing the result makes that result available as an operand for the next operator. The nesting is handled automatically.
package com.bank.core;
import java.util.ArrayDeque;
import java.util.Deque;
public class StackTrace {
public static void main(String[] args) {
String[] tokens = {"100000", "0.02", "*", "500", "+"};
Deque<String> stack = new ArrayDeque<>();
for (String t : tokens) {
if (t.matches("-?[0-9.]+")) {
stack.push(t);
System.out.println("Push " + t + " -> " + stack);
} else {
String b = stack.pop();
String a = stack.pop();
double result = switch (t) {
case "*" -> Double.parseDouble(a) * Double.parseDouble(b);
case "+" -> Double.parseDouble(a) + Double.parseDouble(b);
default -> 0;
};
stack.push(String.valueOf(result));
System.out.println(a + " " + t + " " + b + " = " + result + " -> " + stack);
}
}
}
}
Push 100000 -> [100000]
Push 0.02 -> [0.02, 100000]
100000 * 0.02 = 2000.0 -> [2000.0]
Push 500 -> [500, 2000.0]
2000.0 + 500 = 2500.0 -> [2500.0]
Why does operand order matter?
The first pop is the right operand (b) because it was pushed most recently. The second pop is the left operand (a). For commutative operators like + and * the order does not matter, but for subtraction and division it does. Computing a - b versus b - a gives different results, and swapping them silently produces wrong answers.
package com.bank.core;
import java.util.ArrayDeque;
import java.util.Deque;
public class OperandOrder {
public static void main(String[] args) {
// Expression: 10 3 - means 10 - 3 = 7, not 3 - 10 = -7
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10); // a
stack.push(3); // b
int b = stack.pop(); // 3 (right operand)
int a = stack.pop(); // 10 (left operand)
System.out.println("Correct: a - b = " + (a - b)); // 7
System.out.println("Wrong: b - a = " + (b - a)); // -7
}
}
Correct: a - b = 7
Wrong: b - a = -7
How do you handle banking amounts with BigDecimal?
For money, use BigDecimal and its arithmetic methods. Division must supply an explicit scale and RoundingMode to avoid a non-terminating decimal exception when the result has infinite decimal places, like 1 divided by 3.
package com.bank.service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayDeque;
import java.util.Deque;
public class PostfixBigDecimal {
public static BigDecimal evaluate(String[] tokens) {
Deque<BigDecimal> stack = new ArrayDeque<>();
for (String token : tokens) {
switch (token) {
case "+": { BigDecimal b = stack.pop(); BigDecimal a = stack.pop(); stack.push(a.add(b)); break; }
case "-": { BigDecimal b = stack.pop(); BigDecimal a = stack.pop(); stack.push(a.subtract(b)); break; }
case "*": { BigDecimal b = stack.pop(); BigDecimal a = stack.pop(); stack.push(a.multiply(b)); break; }
case "/": { BigDecimal b = stack.pop(); BigDecimal a = stack.pop();
stack.push(a.divide(b, 2, RoundingMode.HALF_UP)); break; }
default: stack.push(new BigDecimal(token));
}
}
return stack.pop();
}
public static void main(String[] args) {
// 100000 * 0.02 + 500 = 2500.00
String[] feeFormula = {"100000", "0.02", "*", "500", "+"};
System.out.println("Fee: " + evaluate(feeFormula));
// 1000 / 3 = 333.33 (rounded)
String[] divFormula = {"1000", "3", "/"};
System.out.println("Division: " + evaluate(divFormula));
}
}
Fee: 2500.00
Division: 333.33
What happens with an invalid expression?
A well-formed postfix expression leaves exactly one value on the stack. If more than one value remains, there are extra operands. If the stack has fewer than two operands when an operator arrives, the expression is malformed. Both cases should be detected and rejected.
package com.bank.service;
import java.util.ArrayDeque;
import java.util.Deque;
public class PostfixValidation {
public static String evaluateWithValidation(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : tokens) {
if (token.matches("-?\\d+")) {
stack.push(Integer.parseInt(token));
} else {
if (stack.size() < 2) return "Error: not enough operands for " + token;
int b = stack.pop();
int a = stack.pop();
switch (token) {
case "+" -> stack.push(a + b);
case "-" -> stack.push(a - b);
case "*" -> stack.push(a * b);
case "/" -> {
if (b == 0) return "Error: division by zero";
stack.push(a / b);
}
default -> { return "Error: unknown operator " + token; }
}
}
}
if (stack.size() != 1) return "Error: " + stack.size() + " values remain";
return "Result: " + stack.pop();
}
public static void main(String[] args) {
System.out.println(evaluateWithValidation(new String[]{"3", "4", "+"})); // Result: 7
System.out.println(evaluateWithValidation(new String[]{"3", "+"})); // Error
System.out.println(evaluateWithValidation(new String[]{"3", "4", "5", "+"})); // Error: 2 values remain
System.out.println(evaluateWithValidation(new String[]{"10", "0", "/"})); // Error: division by zero
}
}
Result: 7
Error: not enough operands for +
Error: 2 values remain
Error: division by zero
What are common mistakes?
The most common mistake is swapping the operand order for subtraction and division. Another is using double for money, which introduces floating-point rounding errors. A third is not handling division by zero, which throws ArithmeticException for integer division and produces Infinity for doubles.
package com.bank.core;
public class PostfixMistakes {
public static void main(String[] args) {
// Mistake 1: wrong operand order
// "10 3 -" should be 10 - 3 = 7, not 3 - 10 = -7
System.out.println("Always: first pop = right, second pop = left");
// Mistake 2: double for money
double wrong = 0.1 + 0.2;
System.out.println("double: 0.1 + 0.2 = " + wrong); // 0.30000000000000004
java.math.BigDecimal correct = new java.math.BigDecimal("0.1")
.add(new java.math.BigDecimal("0.2"));
System.out.println("BigDecimal: 0.1 + 0.2 = " + correct); // 0.3
}
}
Always: first pop = right, second pop = left
double: 0.1 + 0.2 = 0.30000000000000004
BigDecimal: 0.1 + 0.2 = 0.3
How is this applied in banking?
At National Bank of India, the fee-calculation engine stores pricing formulas in postfix form because they are compact and unambiguous. A formula like 100000 0.02 * 500 + computes 2% of the principal plus a flat charge. Storing formulas as data lets the pricing team change fee logic without redeploying code, and the stack evaluator guarantees exact, correctly rounded amounts using BigDecimal.
package com.bank.service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayDeque;
import java.util.Deque;
public class FeeEngine {
public static BigDecimal computeFee(String[] formula) {
Deque<BigDecimal> stack = new ArrayDeque<>();
for (String token : formula) {
switch (token) {
case "+" -> { BigDecimal b = stack.pop(); stack.push(stack.pop().add(b)); }
case "-" -> { BigDecimal b = stack.pop(); stack.push(stack.pop().subtract(b)); }
case "*" -> { BigDecimal b = stack.pop(); stack.push(stack.pop().multiply(b)); }
case "/" -> { BigDecimal b = stack.pop(); stack.push(stack.pop().divide(b, 2, RoundingMode.HALF_UP)); }
default -> stack.push(new BigDecimal(token));
}
}
return stack.pop();
}
public static void main(String[] args) {
// 2% of 100000 + flat 500 = 2500.00
String[] formula = {"100000", "0.02", "*", "500", "+"};
System.out.println("Processing fee: " + computeFee(formula));
// (principal * rate - discount) = (500000 * 0.015 - 1000) = 6500.00
String[] formula2 = {"500000", "0.015", "*", "1000", "-"};
System.out.println("Loan processing fee: " + computeFee(formula2));
}
}
Processing fee: 2500.00
Loan processing fee: 6500.000
What should a senior engineer know?
Postfix evaluation is one direction of the Shunting-Yard algorithm. The full pipeline is: parse infix to postfix using the Shunting-Yard algorithm, then evaluate postfix with the stack. The Shunting-Yard handles precedence and associativity during conversion, so the evaluator itself never needs to know about them. For production fee engines, this two-phase approach keeps the evaluator simple and the precedence logic isolated.
package com.bank.core;
public class SeniorNotes {
public static void main(String[] args) {
System.out.println("Full pipeline: infix -> Shunting-Yard -> postfix -> stack eval");
System.out.println("Shunting-Yard handles precedence and associativity");
System.out.println("Stack evaluator is always O(n) with no precedence logic");
System.out.println("Production: store formulas as postfix data, evaluate on demand");
}
}
Full pipeline: infix -> Shunting-Yard -> postfix -> stack eval
Shunting-Yard handles precedence and associativity
Stack evaluator is always O(n) with no precedence logic
Production: store formulas as postfix data, evaluate on demand
Visual Overview
Postfix Evaluation: "100000 0.02 * 500 +"
───────────────────────────────────────────
Token Action Stack
─────── ──────────────── ──────────────
100000 push [100000]
0.02 push [0.02, 100000]
* pop 0.02, 100000 [2000]
compute 100000*0.02
push 2000
500 push [500, 2000]
+ pop 500, 2000 [2500]
compute 2000+500
push 2500
Result: 2500 (one value remains)
Operand Order on Pop ───────────────────── Stack top → bottom: [ b | a | ... ] First pop → b (right operand) Second pop → a (left operand) Compute: a OP b For "10 3 -": a=10, b=3 → 10 - 3 = 7
Key Takeaway
Evaluating a postfix expression is the cleanest demonstration of why Reverse Polish Notation exists: operators follow their operands, so the token order already encodes the evaluation order with no parentheses and no precedence rules. My approach is a single left-to-right scan with a stack. Every number I push, and every operator triggers popping the top two values, applying the operation, and pushing the result. When the scan ends, exactly one value remains.
The detail I am always careful about is operand order. The first pop is the right operand because it was pushed most recently, and the second pop is the left operand. For subtraction and division, swapping these silently produces wrong answers, so I compute left-operator-right deliberately. For a banking fee engine this is not cosmetic; a mis-ordered subtraction would quietly overcharge or undercharge a customer.
Because this is money, I keep BigDecimal on the stack rather than primitives, and for division I always supply an explicit scale and RoundingMode.HALF_UP so I get correctly rounded amounts instead of a non-terminating-decimal exception. I also validate the expression: a well-formed postfix string leaves exactly one value, so if values are left over or run out mid-scan, I reject it rather than returning something meaningless.
In my project at the National Bank of India, the fee-calculation engine stores pricing formulas in postfix form because they are compact and unambiguous. Storing formulas as data lets the pricing team change fee logic without redeploying code, and the stack evaluator guarantees exact, correctly rounded amounts in a single O(n) pass.
Remember one sentence: push numbers, pop two on every operator with the correct left-right order, and the single remaining value is the answer.
---