Designing Interfaces and Abstractions
1. Designing Interface Contracts
| Element | Specify |
| Preconditions | Input constraints (Javadoc @param) |
| Postconditions | Return guarantees |
| Exceptions | What thrown when |
| Side effects | State changes, I/O |
| Thread-safety | Document explicitly |
2. Defining Abstract Classes
| Use When | Reason |
| Shared state | Common fields across subtypes |
| Template method | Skeleton + abstract steps |
| Partial implementation | Default behavior + extension points |
| Constructor logic | Forced initialization |
3. Understanding Interface vs Abstract Class
| Aspect | Interface | Abstract Class |
| Multiple inheritance | Yes | No |
| State (fields) | Constants only | Yes |
| Constructor | No | Yes |
| Default methods | Yes Java 8+ | Yes |
| Versioning | Add default | Add concrete |
4. Implementing Multiple Interfaces (composition)
Example: Combine roles
interface Readable<T> { T read(long id); }
interface Writable<T> { void write(T t); }
interface CrudRepo<T> extends Readable<T>, Writable<T> {}
| Conflict | Resolution |
| Same default method | Override and call I.super.method() |
| Diamond default | Compiler forces override |
5. Designing Marker Interfaces
| Marker | Purpose |
Serializable | Opt-in serialization |
Cloneable | Permit clone() |
| Custom marker | Type-level tagging |
| Modern alternative | Annotations + retention |
6. Implementing Default Methods
| Use | Detail |
| API evolution | Add method without breaking implementers |
| Convenience | Derived methods on top of abstract |
| Mixin-like | Behavior across unrelated classes |
Example: Default convenience
interface Repository<T> {
Optional<T> findById(long id);
default T getOrThrow(long id) {
return findById(id).orElseThrow(() -> new NotFoundException(id));
}
}
7. Implementing Functional Interfaces
| Standard | Signature |
Function<T,R> | R apply(T) |
Predicate<T> | boolean test(T) |
Consumer<T> | void accept(T) |
Supplier<T> | T get() |
BiFunction<T,U,R> | R apply(T,U) |
| Custom | Annotate @FunctionalInterface |
8. Designing API Contracts
| Principle | Detail |
| Stable signature | Don't change params on existing methods |
| Add, don't remove | New methods OK; deprecate before removal |
| Semantic versioning | MAJOR for breaking |
| Document errors | Exception types and conditions |
9. Using Interface Segregation
Example: Split a fat interface
// Before
interface UserService { create(); update(); delete(); search(); export(); audit(); }
// After
interface UserCommands { create(); update(); delete(); }
interface UserQueries { search(); }
interface UserOps { export(); audit(); }
10. Implementing Dependency Abstraction
| Pattern | Where defined |
| Port | Domain/application module |
| Adapter | Infrastructure module |
| Wire-up | Composition root (main / DI config) |
11. Designing Sealed Interfaces
Example: Closed hierarchy Java 17+
public sealed interface Result<T> permits Ok, Err {}
public record Ok<T>(T value) implements Result<T> {}
public record Err<T>(String message) implements Result<T> {}
// Exhaustive switch
String describe(Result<Integer> r) {
return switch (r) {
case Ok<Integer> ok -> "value=" + ok.value();
case Err<Integer> e -> "error=" + e.message();
};
}
12. Implementing Interface Evolution
| Change | Strategy |
| Add method | Provide default implementation |
| Deprecate method | @Deprecated(since, forRemoval) |
| Replace method | Add new + delegate from old |
| Breaking change | New interface (V2), keep old until cutover |
Note: Adding a method without default breaks all implementers — always provide a default for backward compatibility.