Separation of Concerns Explained with Java Examples
Introduction
This tutorial explains Separation of Concerns (SoC), one of the oldest and most widely applied principles in software design, with a Java example. We will start with what a "concern" is and where the principle comes from. We will then take a payroll import method that mixes four concerns, and separate them into four small classes, comparing the output of both. Next, we will see how the same principle shapes layered applications and how cross-cutting concerns such as logging are handled. Lastly, we will compare SoC with the Single Responsibility Principle, and look at the ways it can be overdone.What is Separation of Concerns
A concern is any distinct part of a problem that a program has to deal with: reading an input format, validating data, applying business rules, storing data, presenting results, logging, security. Separation of Concerns says that each concern should be handled in its own part of the program, with as little overlap as possible, so that each part can be understood and changed on its own.Edsger W. Dijkstra used the phrase in his 1974 essay On the role of scientific thought, describing it as "the only available technique for effective ordering of one's thoughts": you study one aspect of a problem in isolation, knowing that it is only one aspect. In code, separating concerns lets a reader understand the tax rules without reading the CSV parsing, and lets a developer change the CSV parsing without risking the tax rules.
What mixed concerns look like
Concerns are usually mixed one line at a time. A method that starts as "read the file and print a total" soon also skips bad lines, applies tax and formats a report. The diagram shows such a method with each line coloured by its concern, next to the same work split into four classes.
Example - separating a payroll import
The code below imports a small CSV of employee names and gross pay, skips invalid lines, applies a 20% tax and prints net pay with a total. It does it twice: once in a single method, and once with one class per concern.Java code of a payroll import with mixed concerns and with separated concerns
//SeparationOfConcerns.java
import java.util.ArrayList;
import java.util.List;
public class SeparationOfConcerns {
static final String CSV = """
Tom Jones,5200
Harry Major,4100
,3000
Ethan Hardy,-50
Nancy Smith,6300
""";
// ---------- Mixed: every concern in one method ----------
static void importPayrollMixed(String csv) {
double total = 0;
for (String line : csv.strip().split("\n")) {
String[] f = line.split(",");
if (f[0].isBlank()) continue;
double pay = Double.parseDouble(f[1]);
if (pay < 0) continue;
double tax = pay * 0.2;
total += pay - tax;
System.out.printf("%-12s %9.2f%n", f[0], pay - tax);
}
System.out.printf("%-12s %9.2f%n", "TOTAL", total);
}
// ---------- Separated: one class per concern ----------
// Validation: what a valid entry is
record PayrollEntry(String name, double grossPay) {
PayrollEntry {
if (name.isBlank()) throw new IllegalArgumentException("missing name");
if (grossPay < 0) throw new IllegalArgumentException("negative pay");
}
}
// Input format: CSV text to entries
static class CsvPayrollParser {
final List<String> rejected = new ArrayList<>();
List<PayrollEntry> parse(String csv) {
List<PayrollEntry> entries = new ArrayList<>();
for (String line : csv.strip().split("\n")) {
String[] f = line.split(",");
try {
entries.add(new PayrollEntry(f[0].strip(), Double.parseDouble(f[1].strip())));
} catch (IllegalArgumentException e) {
rejected.add(line + " (" + e.getMessage() + ")");
}
}
return entries;
}
}
// Business rule: tax and net pay
static class NetPayCalculator {
private final double taxRate;
NetPayCalculator(double taxRate) { this.taxRate = taxRate; }
double netPay(PayrollEntry e) { return e.grossPay() * (1 - taxRate); }
}
// Output: how results are shown
static class PayrollReport {
String render(List<PayrollEntry> entries, NetPayCalculator calculator) {
StringBuilder out = new StringBuilder();
double total = 0;
for (PayrollEntry e : entries) {
double net = calculator.netPay(e);
total += net;
out.append(String.format("%-12s %9.2f%n", e.name(), net));
}
return out.append(String.format("%-12s %9.2f%n", "TOTAL", total)).toString();
}
}
public static void main(String[] args) {
System.out.println("Mixed version:");
importPayrollMixed(CSV);
System.out.println("Separated version:");
CsvPayrollParser parser = new CsvPayrollParser();
List<PayrollEntry> entries = parser.parse(CSV);
System.out.print(new PayrollReport().render(entries, new NetPayCalculator(0.20)));
System.out.println("Rejected: " + parser.rejected);
}
}
Mixed version: Tom Jones 4160.00 Harry Major 3280.00 Nancy Smith 5040.00 TOTAL 12480.00 Separated version: Tom Jones 4160.00 Harry Major 3280.00 Nancy Smith 5040.00 TOTAL 12480.00 Rejected: [,3000 (missing name), Ethan Hardy,-50 (negative pay)]
Explanation of the code
- Both versions print the same net pay figures, but the mixed version silently drops the two bad lines. Reporting them would mean adding a fifth concern to the same loop.
PayrollEntryholds the validation rules. Because its constructor rejects invalid data, the calculator and the report never have to check for blank names or negative pay.CsvPayrollParseronly knows the input format. Supporting a JSON file means writing aJsonPayrollParserthat returns the sameList<PayrollEntry>; nothing else changes.NetPayCalculatoronly knows the tax rule, and receives the rate from outside. A different rate, or a progressive tax, changes this class alone, and it can be unit-tested with a singlePayrollEntry.PayrollReportonly knows the layout. It returns aStringinstead of printing, so the caller decides where the report goes - the console, a file or an HTTP response.main()is the only place where the four parts meet. It reads like a description of the process: parse, render, show what was rejected.
Separation of concerns in a whole application
The same principle, applied at the level of a whole application, gives the familiar layered architecture. Each layer handles one concern - talking to users, running use cases, enforcing business rules, storing data - and talks only to the layer below it.
- Methods and classes: one task per method, one responsibility per class.
- Packages and modules:
payroll.domain,payroll.webandpayroll.persistence, with Java modulesClick to read: Java modules to enforce which may use which. - MVC: model (data and rules), view (presentation) and controller (input handling).
- The web itself: HTML for structure, CSS for appearance and JavaScript for behaviour.
- Services: separate deployable services for billing, payroll and notifications.
Cross-cutting concerns
Some concerns cannot live in one layer, because every layer needs them: logging, security checks, transactions, metrics and caching. These are called cross-cutting concerns. Written by hand, they get copied into every method - a log line at the start, a permission check, a try-finally for timing - and the method's real purpose is buried.The usual ways to keep them separate are -
- Decorators: wrap an object with another that adds the concern and delegates the rest, as shown in Composition over InheritanceClick to read tutorial: Composition over Inheritance in Java with Examples and the Decorator patternClick to read: Decorator pattern.
- Filters and interceptors: servlet filters, Spring interceptors and JAX-RS filters handle a concern for every request at one point.
- Aspect-oriented programming (AOP): Spring annotations such as
@Transactionaland@Cacheableadd behaviour around a method without changing its code.
SoC and the Single Responsibility Principle
The two are closely related, but work at different levels. The Single Responsibility PrincipleClick to read: Single Responsibility Principle is about classes, and defines a responsibility as a reason to change. Separation of Concerns is a general principle that applies at every level, from a method to a system of services. A class that follows SRP is an example of SoC; SoC also tells you how to arrange packages, layers and services.Overdoing it
- Layers that only pass data through: a controller that calls a service that calls a repository, where the service adds nothing, is ceremony. Small applications can merge layers.
- Splitting what changes together: if two classes always change in the same commit, they may be one concern split in two. Separate along lines of independent change.
- Too many tiny pieces: a reader who has to open ten files to follow one simple operation has a different problem. Keep it simple - see DRY, KISS and YAGNIClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples.
Summary
In this tutorial we first saw what a concern is and how Dijkstra described Separation of Concerns. Next we looked at how concerns get mixed, one line at a time. We then split a payroll import into a validated record, a parser, a calculator and a report, and saw that each future change touches one class. We then looked at SoC in layered applications and at every scale from methods to services. We then saw how decorators, filters and AOP keep cross-cutting concerns such as logging apart. Lastly, we compared SoC with the Single Responsibility Principle and listed the ways the principle can be overdone.Programming Principles Tutorials
Tell, Don't Ask Principle Explained with Java ExamplesClick to read tutorial: Tell, Don't Ask Principle Explained with Java Examples Immutability in Java: Principles and PracticeClick to read tutorial: Immutability in Java: Principles and Practice SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples Coupling and Cohesion Explained with Java ExamplesClick to read tutorial: Coupling and Cohesion Explained with Java Examples DRY, KISS and YAGNI Principles with Java ExamplesClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC
Tell, Don't Ask Principle Explained with Java ExamplesClick to read tutorial: Tell, Don't Ask Principle Explained with Java Examples Immutability in Java: Principles and PracticeClick to read tutorial: Immutability in Java: Principles and Practice SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples Coupling and Cohesion Explained with Java ExamplesClick to read tutorial: Coupling and Cohesion Explained with Java Examples DRY, KISS and YAGNI Principles with Java ExamplesClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC