Modularity and Interfaces Tutorial from Scratch (2026)
In my fifteen years of building and maintaining large-scale distributed systems, I have learned that modularity is the single most important architectural property a codebase can have. Without clear module boundaries and well-defined interfaces, even a moderately sized project quickly degenerates into a tangled mess of implicit dependencies and hidden side effects.
This tutorial explores the principles of modular design — cohesion, coupling, interface segregation, and API design — with practical examples in Java. By the end, you will know how to structure your code into independent, replaceable modules that communicate through clean abstractions.
Cohesion and Coupling — The Two Forces of Module Design
Cohesion measures how closely related the responsibilities within a single module are. High cohesion means a module has a single, well-defined purpose — all its elements work together toward that goal. Coupling measures how much one module depends on another. Low coupling is desirable because it allows modules to be changed, tested, and deployed independently.
The goal of modular design is to maximize cohesion and minimize coupling. A module that does one thing well and depends on few external modules is easy to understand, test, and maintain. Conversely, a module that does many unrelated things and is tightly coupled to others is a maintenance nightmare.
// High cohesion: this class has a single responsibility
public class EmailValidator {
public boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
}
// Low coupling: depends only on a stable interface, not a concrete class
public class NotificationService {
private final MessageSender sender;
public NotificationService(MessageSender sender) {
this.sender = sender;
}
public void notify(String message) {
sender.send(message);
}
}
Interface Segregation — Small, Focused Contracts
Interface Segregation Principle (ISP) states that no client should be forced to depend on methods it does not use. Instead of creating one large, monolithic interface, you should create several small, focused interfaces. This prevents implementing classes from being burdened with irrelevant methods and reduces the blast radius of changes.
In practice, this means designing interfaces that are role-specific. A class that needs to read data should depend on a Readable interface, not a ReadableWritableDeletable interface. This granularity also makes it easier to mock dependencies in tests.
// Bad: fat interface
public interface Worker {
void work();
void eat();
void sleep();
}
// Good: segregated interfaces
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
// A robot only implements what it needs
public class Robot implements Workable {
@Override
public void work() {
System.out.println("Robot working...");
}
}
API Design Principles for Modules
Designing a module's public API is like designing a user interface — it should be intuitive, consistent, and hard to misuse. Good API design follows the Principle of Least Astonishment: the API should behave in a way that users expect. Method names should be descriptive, parameters should be well-ordered, and return types should convey meaningful information.
Additional principles include: make the common case easy (and the uncommon case possible but not necessarily easy), prefer immutable objects to avoid shared mutable state across modules, and document all public methods with clear preconditions and postconditions.
public class FileRepository {
private final Path storagePath;
public FileRepository(Path storagePath) {
this.storagePath = storagePath;
}
// Clear, descriptive method name with consistent parameter order
public Optional readFile(String fileName) {
Path filePath = storagePath.resolve(fileName);
if (!Files.exists(filePath)) {
return Optional.empty();
}
return Optional.of(Files.readString(filePath));
}
// Returns meaningful type instead of void boolean flags
public WriteResult writeFile(String fileName, String content) {
// implementation
return WriteResult.SUCCESS;
}
}
Frequently Asked Questions
What is the difference between cohesion and coupling?
Cohesion measures how related the elements within a single module are (internal consistency), while coupling measures how dependent one module is on another module (external dependency). High cohesion and low coupling is the ideal.
How do you achieve low coupling in a large codebase?
Use dependency injection, depend on interfaces rather than concrete classes, apply the Facade pattern to hide subsystem complexity, and follow the Law of Demeter (talk only to your immediate friends).
What is the Interface Segregation Principle?
ISP states that no client should be forced to depend on methods it does not use. Instead of one large interface, create multiple smaller, focused interfaces so that implementing classes only need to provide the behavior that is relevant to them.
Should every class have an interface?
No. Only extract interfaces when you have a genuine need for polymorphism, multiple implementations, or testability concerns. Premature interface extraction adds unnecessary indirection without benefit.
Originally published on Ayodhyyya. Last updated June 1, 2026.