Functional programming is a programming style built around functions, immutable data and expressions that transform values. It does not replace object-oriented or procedural programming, and modern languages such as Java, JavaScript and Python routinely mix all three styles.
The value of functional programming is that it encourages code that is easier to reason about, compose, test and parallelize. Developers can adopt these ideas incrementally without switching to a purely functional language.
What is functional programming?
Functional programming treats computation primarily as the evaluation and composition of functions. Instead of repeatedly changing shared state, functional code favors transformations in which input values produce new output values.
The following seven ideas provide a practical introduction:
- Functions are first-class values.
- Pure functions are deterministic.
- Pure functions avoid observable side effects.
- Data is treated as immutable.
- Functional code favors declarative transformations.
- Small functions are composed into larger operations.
- Recursion and higher-order operations can replace explicit mutation-based loops.
1. Functions are first-class values
A language has first-class functions when functions can be treated like other values. They can be stored in variables, passed as arguments and returned from other functions.
A higher-order function accepts a function as an argument, returns a function, or both. Java's lambda expressions and functional interfaces make this style possible even though Java remains an object-oriented language.
import java.util.function.Function;
Function<Integer, Integer> doubleIt =
number -> number * 2;
static int apply(
int value,
Function<Integer, Integer> operation) {
return operation.apply(value);
}The function itself becomes data that can be passed to another operation. Java Streams make extensive use of this technique through functions passed to methods such as map, filter and reduce.
2. Pure functions are deterministic
A pure function returns the same result whenever it receives the same inputs. Its result does not secretly depend on mutable global state, the current time, random values, network responses or other changing external information.
function upperFirstFive(text) {
const first = text.slice(0, 5).toUpperCase();
const rest = text.slice(5).toLowerCase();
return first + rest;
}
upperFirstFive("abcdefghi"); // ABCDEfghi
upperFirstFive("lmnopqrstuv"); // LMNOpqrstuv
upperFirstFive("abcd"); // ABCDGiven the same string, upperFirstFive always produces the same result. Determinism makes behavior easier to test and reason about.
3. Pure functions avoid side effects
A side effect is an observable interaction with state outside a function's returned value. Examples include modifying global variables, changing an object supplied by the caller, writing a file, updating a database or printing to the console.
Side effects are unavoidable in useful applications. The functional programming goal is not to pretend they do not exist, but to keep them controlled and separate them from pure transformations when practical.
This JavaScript function is problematic because it modifies shared state:
let taxRate = 0.05;
function determineTotal(price, state) {
if (state.toUpperCase() === "NH") {
taxRate = 0;
}
return price + (price * taxRate);
}After a New Hampshire calculation, taxRate remains zero. A later call can therefore return a different result because of an earlier call.
A pure alternative derives everything from its arguments:
function determineTotal(price, state) {
const taxRate =
state.toUpperCase() === "NH" ? 0 : 0.05;
return price + (price * taxRate);
}The revised function changes no external state, and its output depends only on its inputs.
4. Favor immutable data
Functional programming favors values that are not modified after creation. Instead of changing an existing value, an operation produces a new value.
record Account(String owner, double balance) {
}
static Account deposit(
Account account,
double amount) {
return new Account(
account.owner(),
account.balance() + amount
);
}The original Account is unchanged. The deposit function returns a new value containing the updated balance.
Immutability is more than applying Java's final keyword to a variable. A final reference cannot be reassigned, but the object referenced by it may still be mutable. Truly immutable designs prevent the object's observable state from changing after construction.
5. Favor declarative transformations
Imperative code emphasizes the individual steps required to perform an operation. Declarative code emphasizes the transformation or result.
For example, this Java code explicitly controls iteration and mutation:
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.length() >= 5) {
result.add(name.toUpperCase());
}
}The Stream API expresses the same transformation more declaratively:
List<String> result = names.stream()
.filter(name -> name.length() >= 5)
.map(String::toUpperCase)
.toList();The second example describes a pipeline: filter the values, transform them and collect the result. It delegates the mechanics of iteration to the Stream API.
6. Compose small functions
Function composition combines small operations to create more sophisticated behavior. Each component can remain focused and independently testable.
import java.util.function.Function;
Function<String, String> trim =
String::trim;
Function<String, String> uppercase =
String::toUpperCase;
Function<String, String> normalize =
trim.andThen(uppercase);
String result =
normalize.apply(" functional java ");
// FUNCTIONAL JAVAJava's Function interface provides compose and andThen specifically for this style of function composition.
Composition also appears naturally in stream pipelines:
int sum = numbers.stream()
.filter(number -> number % 2 == 0)
.mapToInt(Integer::intValue)
.sum();Filtering and summing remain distinct operations, but the pipeline composes them into one calculation.
7. Use recursion and higher-order operations carefully
Pure functional languages often use recursion instead of mutation-based loops. A recursive function repeatedly calls itself until it reaches a base case.
def sum_recursive(n):
if n <= 0:
return 0
return n + sum_recursive(n - 1)
print(sum_recursive(5)) # 15However, recursion should not be treated as a universal replacement for loops. Runtime behavior matters. Some functional languages optimize tail calls, but Java does not generally perform tail-call optimization, and Python also has a finite recursion depth. Deep recursion can therefore exhaust the call stack.
In Java, higher-order collection operations and streams are often a better functional alternative to explicit loops than hand-written recursion.
Pure functions and referential transparency
Another useful functional programming concept is referential transparency. An expression is referentially transparent when it can be replaced by its value without changing the program's behavior.
static int square(int number) {
return number * number;
}
int result = square(5) + square(5);Because square(5) always evaluates to 25 and has no side effects, replacing either invocation with 25 does not change the program's behavior.
Functional programming is not all or nothing
Most mainstream applications are not purely functional. A web application must read requests, access databases, write logs and return responses. Those operations inherently interact with the outside world.
A practical approach is to keep as much business logic as possible in deterministic, side-effect-free functions and move unavoidable effects toward clearly defined boundaries.
| Principle | Practical goal |
|---|---|
| First-class functions | Pass behavior as data |
| Determinism | Same inputs produce the same outputs |
| Controlled side effects | Keep external state changes explicit |
| Immutability | Create new values instead of modifying old ones |
| Declarative programming | Describe transformations instead of iteration mechanics |
| Composition | Build complex behavior from small functions |
| Recursion and higher-order operations | Avoid unnecessary mutation while respecting runtime limits |
Why functional programming matters
Functional techniques reduce the amount of mutable state a developer must track mentally. Pure functions are straightforward to unit test because their behavior is determined by their inputs. Immutable values also reduce the risk that one part of a program unexpectedly changes data used elsewhere.
Java developers already encounter functional ideas through lambda expressions, method references, functional interfaces, Optional, the Stream API and immutable records. Learning the principles behind those features makes them easier to use effectively.
Functional programming is therefore best understood not as a replacement for object-oriented programming, but as another set of design tools. Modern applications can combine objects for modeling and encapsulation with functional transformations for predictable data processing.
Bob Reselman is a software developer, system architect and writer. His expertise ranges from software development technologies to techniques and culture.