Professional Java Practices
Designing Clear APIs and Boundaries
You are viewing a free preview lesson.
Lesson Overview
একটি class compile করছে এবং correct result দিচ্ছে—এটাই professional Java design-এর শেষ কথা নয়।
একটি ভালো API এমন হওয়া উচিত যাতে caller সহজেই বুঝতে পারে:
কী করতে পারবে
কী করতে পারবে না
কোন input valid
কোন result expect করা যায়
failure কীভাবে প্রকাশ পাবে
object-এর কোন state caller change করতে পারবে
Consider:
void process(
String value,
int type,
boolean flag
)
এই method compile করে।
কিন্তু caller-এর জন্য অনেক প্রশ্ন:
value কী?
type 1 মানে কী?
flag true মানে কী?
method কী process করছে?
failure হলে কী হবে?
Compare:
Enrollment enroll(
LearnerId learnerId,
CourseCode courseCode
)
এখানে intent অনেক বেশি clear।
Professional API design-এর goal হলো:
caller-এর জন্য correct use সহজ করা
এবং incorrect use কঠিন করা।
এই lesson-এ আমরা শিখব:
- API এবং boundary কী
- Public surface area
- Information hiding
- Small APIs
- Strong parameter types
- Clear return types
- Null policies
- Empty collections
- Optional results
- Command এবং query separation
- Side effects
- Boundary validation
- Ownership
- Mutable collection exposure
- Domain exceptions
- Boolean blindness
- Primitive obsession
- Utility class overuse
- Leaky abstractions
- Stable contracts
- Practical API review
What Is an API?
API মানেই HTTP API নয়।
একটি Java class-এর public methods-ও একটি API।
Example:
public final class CourseService {
public Course create(
CourseCode code,
String title
) {
...
}
public Course find(
CourseCode code
) {
...
}
}
Other code interacts with CourseService through these methods।
That public surface is an API।
Internal API vs External API
Within one Java application:
Class A
→ calls Class B
এই interaction-এরও contract আছে।
External boundary হতে পারে:
HTTP
file
database
message broker
third-party service
Internal boundary হতে পারে:
Service
Repository
Domain Object
Utility
Component
Good boundary design দুই ক্ষেত্রেই important।
API Design Is Contract Design
Suppose:
Course findByCode(
String code
);
Questions:
code null হতে পারে?
blank হতে পারে?
not found হলে null?
exception?
some default Course?
Method signature যত বেশি semantics communicate করতে পারে, caller-এর guess তত কম লাগে।
Prefer Meaningful Types
Instead of:
Course findByCode(
String code
);
consider:
Optional<Course> findByCode(
CourseCode code
);
Now types communicate:
CourseCode
→ arbitrary String নয়
Optional<Course>
→ Course নাও থাকতে পারে
This is stronger API design।
Small Public Surface Area
Suppose class:
public final class Course {
public void setCode(...) {
}
public void setTitle(...) {
}
public void setStatus(...) {
}
public void setLessons(...) {
}
public void setPrice(...) {
}
public void resetEverything(...) {
}
public void internalNormalize(...) {
}
public void validateInternalState(...) {
}
}
Caller এখন almost everything control করতে পারে।
এটি dangerous।
Expose Capabilities, Not Internals
Better:
public final class Course {
public void changeTitle(
String title
) {
...
}
public void addLesson(
Lesson lesson
) {
...
}
public void publish() {
...
}
public void archive() {
...
}
}
Now API reflects:
meaningful domain operations
not raw internal state mutation।
Information Hiding
Information hiding means:
caller জানবে কী করতে পারে
কিন্তু implementation-এর unnecessary details জানতে হবে না
Example:
course.publish();
Caller-এর জানার দরকার নেই publish internally:
status field change করে
lesson count validate করে
timestamp set করে
event তৈরি করে
Those are implementation responsibilities।
Avoid Public Setters by Default
Setter:
course.setStatus(
CourseStatus.PUBLISHED
);
allows direct transition।
But perhaps business rule says:
DRAFT Course with zero Lessons cannot be published
Setter bypasses invariant।
Better:
course.publish();
Inside:
if (
lessons.isEmpty()
) {
throw new IllegalStateException(
"Course must contain lessons."
);
}
status =
CourseStatus.PUBLISHED;
API protects domain rules।
Make Invalid Operations Hard to Express
Bad:
course.setStatus(
CourseStatus.PUBLISHED
);
The caller can choose any status transition।
Better:
course.publish();
course.archive();
Now valid transitions can be enforced inside the object।
Strong Parameter Types
Consider:
void enroll(
long learnerId,
String courseCode
)
This is workable।
But stronger:
void enroll(
LearnerId learnerId,
CourseCode courseCode
)
Now type system communicates meaning।
Primitive Obsession
A method:
void createUser(
String name,
String email,
String country,
String role,
String status
)
uses primitives/basic types for multiple domain concepts।
Potential problems:
values can be swapped
validation repeated
meaning hidden
invalid strings easy to pass
Strong types can help where domain meaning justifies them।
Example — EmailAddress
record EmailAddress(
String value
) {
EmailAddress {
if (
value == null
|| value.isBlank()
) {
throw new IllegalArgumentException(
"Email is required."
);
}
value =
value.strip()
.toLowerCase(
Locale.ROOT
);
}
}
Then:
Learner register(
String name,
EmailAddress email
)
Caller cannot accidentally pass a CourseCode where EmailAddress is expected।
Do Not Create a Type for Everything
Strong types are useful, but overuse creates noise।
This:
CourseTitle
CourseDescription
CourseSubtitle
CourseShortDescription
CourseLongDescription
may be unnecessary if those values have no distinct behavior/invariants।
Create a value type when it adds:
meaning
validation
normalization
type safety
behavior
not simply because every String could theoretically have a wrapper।
Boolean Blindness
Consider:
createCourse(
"Java",
true,
false
);
What do:
true
false
mean?
Caller must inspect method definition।
This is:
Boolean Blindness
Better with Enum
Instead of:
createCourse(
title,
true
);
consider:
createCourse(
title,
CourseVisibility.PUBLIC
);
Enum:
enum CourseVisibility {
PUBLIC,
PRIVATE
}
Much clearer।
Boolean Can Still Be Fine
This is clear:
course.published()
returns:
boolean
Question naturally has yes/no semantics।
Boolean problem is mostly when caller sees:
someMethod(
true,
false,
true
)
without clear meaning।
Method Names Should Reveal Intent
Compare:
process(
course
);
with:
publish(
course
);
or:
calculateEnrollmentPrice(
course,
learner
);
Specific names reduce mental load।
Avoid Generic Verbs Without Meaning
Names such as:
process
handle
execute
manage
perform
doWork
can be appropriate at abstraction boundaries, but often hide actual responsibility।
Ask:
What is this method actually doing?
Then name that behavior।
Return Types Are Part of the Contract
Suppose:
Course findByCode(
CourseCode code
);
Not found semantics unclear।
Potential alternatives:
Optional<Course> findByCode(
CourseCode code
);
or:
Course requireByCode(
CourseCode code
);
where missing means exception।
Different return types communicate different contracts।
find and require
Useful naming distinction:
Optional<Course> findByCode(
CourseCode code
);
means:
absence is normal
while:
Course requireByCode(
CourseCode code
);
can mean:
Course must exist for this operation
and missing state becomes failure।
Avoid Returning null Without a Clear Contract
Bad API:
Course findCourse(
String code
);
and implementation sometimes returns:
null
Caller may forget checking।
If absence is meaningful:
Optional<Course>
can make it explicit।
But Do Not Use Optional Everywhere
Collection result:
List<Course> findPublishedCourses();
If no Courses:
List.of()
is usually better than:
Optional<List<Course>>
because a collection already models:
zero items
Null Policy Should Be Deliberate
For each API, decide:
Can input be null?
Can output be null?
Does null mean absence?
Is null invalid?
Should empty collection be used instead?
Do not let null behavior emerge accidentally।
Required Input
If input must exist:
public Course create(
CourseCode code,
String title
) {
Objects.requireNonNull(
code,
"code is required"
);
...
}
Better still, make CourseCode itself unable to represent invalid code।
Validate at Boundaries
External input is often untrusted।
Examples:
console input
file content
HTTP request
message
external API result
Validate/normalize as data crosses into stronger internal model।
Conceptually:
Raw input
↓
Boundary validation
↓
Domain type
↓
Internal logic
Avoid Revalidating Everywhere
If:
CourseCode
guarantees normalized valid code, downstream methods should not repeatedly write:
if (
code != null
&& !code.isBlank()
)
Trust validated types।
Otherwise strong types provide little benefit।
Command and Query Separation
A useful design principle:
Command
→ changes state
Query
→ returns information
Examples:
course.publish();
Command।
course.status();
Query।
Avoid Surprising Query Side Effects
Bad:
Course findCourse(
CourseCode code
) {
accessCount++;
maybeArchiveOldCourses();
...
}
A caller expecting simple lookup now triggers unrelated mutation।
Sometimes metrics are acceptable implementation effects, but business state mutation from a query should be deliberate and unsurprising।
Command Return Values
Command does not necessarily have to return void।
Example:
Enrollment enroll(
LearnerId learnerId,
CourseCode courseCode
)
changes state and returns the newly created Enrollment।
That is still understandable।
The point is not strict syntax.
The point is:
method purpose should be clear.
Side Effects Should Be Visible in Design
Consider:
List<Course> courses =
service.getCourses();
Caller may assume reading。
If getCourses() also:
deletes expired data
sends emails
updates files
API is surprising।
Avoid hidden major side effects।
Collection Ownership
Suppose class has:
private final List<Lesson> lessons =
new ArrayList<>();
Bad:
public List<Lesson> lessons() {
return lessons;
}
Caller can:
course.lessons()
.clear();
Now caller bypasses:
validation
duplicate rules
course lifecycle
Return a Safe View
Better:
public List<Lesson> lessons() {
return List.copyOf(
lessons
);
}
Caller receives data without direct mutation ownership।
Mutation Through Meaningful Methods
Then:
public void addLesson(
Lesson lesson
) {
...
}
Course owns its state transition।
Boundary Ownership
Ask:
Who owns this mutable object?
If caller receives a mutable List reference, ownership becomes ambiguous।
Better APIs define clear ownership:
caller owns input
component copies it
or
component owns internal data
caller receives immutable snapshot
Defensive Copying on Input
Suppose constructor:
Course(
List<Lesson> lessons
) {
this.lessons =
lessons;
}
Caller can mutate original List later।
Better:
this.lessons =
new ArrayList<>(
lessons
);
or immutable model:
this.lessons =
List.copyOf(
lessons
);
depending on internal needs।
Defensive Copying on Output
Internal mutable List:
private final List<Lesson> lessons =
new ArrayList<>();
Return:
public List<Lesson> lessons() {
return List.copyOf(
lessons
);
}
Now internal state remains owned by Course।
Avoid Leaky Abstractions
Suppose repository API:
Properties loadCourseProperties(
CourseCode code
);
Caller now knows persistence uses:
Properties
This leaks storage implementation।
Better:
Optional<Course> findByCode(
CourseCode code
);
Caller cares about Course, not file format।
Another Leaky Boundary
Bad:
Path saveCourse(
Course course
);
if caller has no legitimate reason to care about storage path।
Better:
void save(
Course course
);
or return domain-relevant result if necessary।
Repository Contract
A clean repository interface might be:
interface CourseRepository {
void save(
Course course
);
Optional<Course> findByCode(
CourseCode code
);
List<Course> findAll();
}
Notice what is absent:
file path
Properties
SQL
JSON
HashMap
Those are implementation details।
Abstraction Boundary
Caller sees:
CourseRepository
Implementation can later be:
FileCourseRepository
InMemoryCourseRepository
DatabaseCourseRepository
without forcing caller to understand storage mechanics।
Avoid Over-General Interfaces
This:
interface Repository<
T,
ID,
FILTER,
SORT,
PAGE,
CONTEXT
> {
...
}
may look reusable but create complexity before requirements justify it।
For a small domain:
interface CourseRepository {
...
}
is often clearer।
Abstraction Must Earn Its Cost
Every abstraction adds:
name
concept
indirection
navigation
maintenance
Create one when it provides real value such as:
stable boundary
multiple implementations
test seam
dependency direction
clear domain contract
Not just because:
professional code has interfaces
Avoid Utility Dumping Grounds
A class:
Utils
eventually gets:
validateCourse()
formatPrice()
normalizeEmail()
calculateDiscount()
readFile()
sendNotification()
These functions have unrelated responsibilities।
This hides domain structure।
Better Placement
Normalization of CourseCode:
CourseCode
Price behavior:
Money
Course lifecycle:
Course
Persistence:
CourseRepository
Formatting:
presentation boundary
Put behavior near the concept it belongs to।
Static Utility Methods Are Not Always Bad
Example:
Math.max(...)
is excellent।
A focused stateless utility can be appropriate।
Problem is:
unrelated behaviors collected because no one decided ownership.
Failure Is Part of API Design
Suppose:
Course publish(
CourseCode code
)
Potential failures:
Course not found
Course has no lessons
Course already published
storage failure
These should not all become:
null
or:
false
without meaningful semantics।
Boolean Failure Can Lose Information
Example:
boolean publish(
CourseCode code
)
False could mean:
not found
already published
invalid state
storage failed
Too ambiguous।
Meaningful Exceptions
Example:
Course course =
repository.findByCode(
code
).orElseThrow(
() ->
new CourseNotFoundException(
code
)
);
Then Course itself may enforce:
course.publish();
and throw appropriate state failure if invariant violated।
Do Not Create Exception Types for Every Line
Custom exception is useful when it adds:
domain meaning
caller handling distinction
useful context
Do not create dozens of meaningless wrappers simply to have custom classes।
Stable APIs Hide Change
Suppose caller uses:
repository.findByCode(
code
);
Today implementation:
HashMap
Tomorrow:
file storage
Later:
database
If API remains stable, caller does not change।
That is a valuable boundary।
Do Not Expose Implementation-Specific Types Accidentally
Suppose:
ArrayList<Course> findAll();
Why force caller to depend on ArrayList?
Better:
List<Course> findAll();
unless ArrayList-specific behavior is truly part of the contract।
Program to the Required Abstraction
If caller needs:
ordered sequence
return:
List<Course>
If caller needs:
unique values
return:
Set<CourseCode>
If lookup mapping matters:
Map<CourseCode, CourseSummary>
Choose return type based on semantics।
Do Not Return Overly Generic Types Either
This:
Collection<Course> findAll();
may hide ordering guarantees caller actually needs।
If order is meaningful and guaranteed, List<Course> communicates more।
Use the narrowest useful semantic contract—not merely the most abstract type possible।
Parameter Object
Suppose method grows:
searchCourses(
String query,
CourseStatus status,
long minPrice,
long maxPrice,
boolean publishedOnly,
int limit
)
Many parameters become difficult to use।
A parameter object may help:
record CourseSearchCriteria(
String query,
CourseStatus status,
long minPriceInPaisa,
long maxPriceInPaisa,
int limit
) {
}
Then:
searchCourses(
criteria
);
Do Not Introduce Parameter Object Too Early
For:
findByCode(
CourseCode code
)
creating:
FindCourseByCodeRequest
may add unnecessary ceremony।
Use parameter objects when parameter grouping has actual meaning।
Avoid Flag-Controlled Multi-Behavior Methods
Bad:
saveCourse(
course,
true
);
where true means:
publish after saving
Better:
save(
course
);
publish(
course.code()
);
or a clearly named operation:
createAndPublish(
request
);
if it is genuinely one use case।
One Method, One Clear Purpose
This does not mean a method can call only one other method।
It means caller should be able to summarize the operation clearly।
Good:
enroll(
learnerId,
courseCode
)
Internally it may:
load learner
load course
validate state
check duplicate
create enrollment
save enrollment
Still one coherent use case:
Enroll learner in course.
Public API and Internal Helpers
Public:
public Enrollment enroll(
LearnerId learnerId,
CourseCode courseCode
)
Internal helpers:
private Course requirePublishedCourse(
CourseCode code
)
private Learner requireLearner(
LearnerId id
)
Caller does not need these implementation details।
Keep public surface small।
Encapsulation Through Access Modifiers
Use:
private
for implementation details।
Use package-private where package collaboration genuinely benefits।
Use:
public
only for intentional API surface।
Do not make methods public merely because testing private behavior directly seems convenient।
Test public behavior where possible।
API Should Express Invariants
Suppose Enrollment starts:
ACTIVE
Do not expose:
setStatus(
EnrollmentStatus status
)
Prefer:
complete();
cancel();
Now valid transitions remain inside entity।
Example Enrollment API
public final class Enrollment {
private EnrollmentStatus status;
public void complete() {
requireActive();
status =
EnrollmentStatus.COMPLETED;
}
public void cancel() {
requireActive();
status =
EnrollmentStatus.CANCELLED;
}
public EnrollmentStatus status() {
return status;
}
private void requireActive() {
if (
status
!= EnrollmentStatus.ACTIVE
) {
throw new IllegalStateException(
"Enrollment is not active."
);
}
}
}
Caller can express only meaningful transitions।
Design APIs Around Domain Language
If business language says:
publish a Course
enroll a Learner
complete an Enrollment
archive a Course
API should often use the same vocabulary:
course.publish();
enrollment.complete();
enrollmentService.enroll(
learnerId,
courseCode
);
This makes code closer to problem domain।
Avoid Technical Names at Domain Boundary
Less expressive:
course.updateStatus(
2
);
Better:
course.publish();
The second communicates domain intent directly।
Complete Example
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
public class Main {
public static void main(
String[] args
) {
CourseRepository repository =
new InMemoryCourseRepository();
CourseService service =
new CourseService(
repository
);
CourseCode code =
new CourseCode(
" java-foundation "
);
service.create(
code,
"Java Foundation"
);
service.addLesson(
code,
new Lesson(
1,
"Introduction to Java"
)
);
service.publish(
code
);
CourseSummary summary =
service.find(
code
);
System.out.println(
summary
);
}
record CourseCode(
String value
) {
CourseCode {
value =
Objects.requireNonNull(
value,
"Course code is required."
)
.strip()
.toUpperCase(
Locale.ROOT
);
if (
value.isBlank()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
}
@Override
public String toString() {
return value;
}
}
record Lesson(
long id,
String title
) {
Lesson {
if (
id <= 0
) {
throw new IllegalArgumentException(
"Lesson ID must be positive."
);
}
title =
Objects.requireNonNull(
title,
"Lesson title is required."
).strip();
if (
title.isBlank()
) {
throw new IllegalArgumentException(
"Lesson title is required."
);
}
}
}
enum CourseStatus {
DRAFT,
PUBLISHED,
ARCHIVED
}
static final class Course {
private final CourseCode code;
private String title;
private CourseStatus status =
CourseStatus.DRAFT;
private final List<Lesson> lessons =
new ArrayList<>();
Course(
CourseCode code,
String title
) {
this.code =
Objects.requireNonNull(
code
);
changeTitle(
title
);
}
CourseCode code() {
return code;
}
String title() {
return title;
}
CourseStatus status() {
return status;
}
List<Lesson> lessons() {
return List.copyOf(
lessons
);
}
void changeTitle(
String title
) {
if (
status
!= CourseStatus.DRAFT
) {
throw new IllegalStateException(
"Only draft courses can be edited."
);
}
title =
Objects.requireNonNull(
title,
"Title is required."
).strip();
if (
title.isBlank()
) {
throw new IllegalArgumentException(
"Title is required."
);
}
this.title =
title;
}
void addLesson(
Lesson lesson
) {
requireDraft();
Objects.requireNonNull(
lesson
);
boolean duplicate =
lessons.stream()
.anyMatch(
existing ->
existing.id()
== lesson.id()
);
if (
duplicate
) {
throw new IllegalArgumentException(
"Duplicate lesson ID: "
+ lesson.id()
);
}
lessons.add(
lesson
);
}
void publish() {
requireDraft();
if (
lessons.isEmpty()
) {
throw new IllegalStateException(
"Course must contain at least one lesson."
);
}
status =
CourseStatus.PUBLISHED;
}
private void requireDraft() {
if (
status
!= CourseStatus.DRAFT
) {
throw new IllegalStateException(
"Course must be in DRAFT status."
);
}
}
}
record CourseSummary(
CourseCode code,
String title,
CourseStatus status,
int lessonCount
) {
}
interface CourseRepository {
void save(
Course course
);
Optional<Course> findByCode(
CourseCode code
);
}
static final class InMemoryCourseRepository
implements CourseRepository {
private final List<Course> courses =
new ArrayList<>();
@Override
public void save(
Course course
) {
Optional<Course> existing =
findByCode(
course.code()
);
if (
existing.isEmpty()
) {
courses.add(
course
);
}
}
@Override
public Optional<Course> findByCode(
CourseCode code
) {
return courses.stream()
.filter(
course ->
course.code()
.equals(
code
)
)
.findFirst();
}
}
static final class CourseService {
private final CourseRepository repository;
CourseService(
CourseRepository repository
) {
this.repository =
Objects.requireNonNull(
repository
);
}
void create(
CourseCode code,
String title
) {
if (
repository.findByCode(
code
).isPresent()
) {
throw new IllegalArgumentException(
"Course already exists: "
+ code
);
}
repository.save(
new Course(
code,
title
)
);
}
void addLesson(
CourseCode code,
Lesson lesson
) {
Course course =
requireCourse(
code
);
course.addLesson(
lesson
);
repository.save(
course
);
}
void publish(
CourseCode code
) {
Course course =
requireCourse(
code
);
course.publish();
repository.save(
course
);
}
CourseSummary find(
CourseCode code
) {
Course course =
requireCourse(
code
);
return new CourseSummary(
course.code(),
course.title(),
course.status(),
course.lessons()
.size()
);
}
private Course requireCourse(
CourseCode code
) {
return repository.findByCode(
code
).orElseThrow(
() ->
new IllegalArgumentException(
"Course not found: "
+ code
)
);
}
}
}
What This Example Demonstrates
Strong Input Type
Instead of:
String code
we use:
CourseCode
which validates and normalizes itself।
Small Domain API
Course exposes:
changeTitle()
addLesson()
publish()
not generic field setters।
Protected Collection Ownership
lessons()
returns:
List.copyOf(
lessons
)
so caller cannot clear internal lessons directly।
Repository Boundary
Service knows:
CourseRepository
not:
ArrayList storage mechanics
Explicit Absence
Repository:
Optional<Course> findByCode(...)
clearly represents lookup absence।
Application Boundary
Service converts:
optional absence
into:
required Course or failure
for operations that require existence।
Read Model
Caller receives:
CourseSummary
rather than mutable Course entity when only summary information is required।
Common Mistake 1 — Too Many Public Methods
Every public method becomes part of the class contract।
Keep implementation helpers:
private
unless callers genuinely need them।
Common Mistake 2 — Generic Setters
setStatus(...)
may let callers bypass lifecycle rules।
Prefer domain operations:
publish()
archive()
complete()
cancel()
Common Mistake 3 — Raw String Everywhere
If several Strings represent distinct concepts and have validation rules, strong domain types may improve safety।
Common Mistake 4 — Boolean Parameters Without Meaning
Avoid:
create(
true,
false
)
when enums or meaningful methods would make intent clearer।
Common Mistake 5 — Hidden Null Contract
Do not make callers guess whether:
find(...)
returns null।
Use explicit contract।
Common Mistake 6 — Optional Collection
Usually:
List<Course>
with empty List is enough।
Avoid unnecessary:
Optional<List<Course>>
Common Mistake 7 — Leaking Internal Collections
Avoid:
return lessons;
when caller should not own mutation।
Common Mistake 8 — Leaking Persistence Details
Repository callers should not need to know:
file paths
Properties
SQL rows
HashMap internals
unless those details genuinely belong to their responsibility।
Common Mistake 9 — Over-Generalizing Too Early
Do not create generic frameworks before you have actual repeated requirements।
Simple specific APIs are often easier to maintain।
Common Mistake 10 — Giant Utility Class
Place behavior near the concept it belongs to instead of accumulating unrelated methods in:
Utils
Common Mistake 11 — Ambiguous boolean Return
boolean execute()
may hide too many failure cases।
Use richer result or meaningful exception when caller needs to know why something failed।
Common Mistake 12 — Query with Surprising Mutation
A method named:
findCourse()
should not unexpectedly alter major business state।
Keep API effects predictable।
Practice 1
Which is clearer?
void enroll(
long id,
String code
)
or:
void enroll(
LearnerId learnerId,
CourseCode courseCode
)
Answer
The second, when these domain types have meaningful semantics।
Practice 2
Why is this potentially problematic?
course.setStatus(
CourseStatus.PUBLISHED
);
Answer
Caller can bypass publication rules।
A domain method:
course.publish();
can enforce invariants।
Practice 3
What should a lookup method return when result may legitimately not exist?
Answer
Often:
Optional<T>
is a strong choice for a single result।
Practice 4
No Courses found।
Which is usually better?
Optional<List<Course>>
or:
List<Course>
Answer
Usually:
List<Course>
with empty List।
Practice 5
Why can this be dangerous?
public List<Lesson> lessons() {
return lessons;
}
Answer
Caller can directly mutate internal collection and bypass invariants।
Practice 6
Better version?
Answer
public List<Lesson> lessons() {
return List.copyOf(
lessons
);
}
if caller only needs a read-only snapshot।
Practice 7
What is wrong with:
save(
course,
true
);
if true means publish?
Answer
Boolean argument does not communicate intent clearly।
A meaningful operation or enum may be clearer।
Practice 8
Should repository API expose:
Properties
because implementation stores files using Properties?
Answer
Usually no।
That leaks persistence details through the abstraction boundary।
Practice 9
What should determine whether a method is public?
Answer
Whether callers intentionally need that capability as part of the contract—not convenience for implementation or testing।
Practice 10
When is a custom value type useful?
Answer
When it adds meaningful:
validation
normalization
domain meaning
type safety
behavior
Practice 11
Does every String need a wrapper record?
Answer
No।
Strong types should earn their complexity।
Practice 12
What is the problem with:
boolean publish(
CourseCode code
)
if false can mean four different failures?
Answer
Return value loses failure meaning।
Use an API that communicates relevant failure states more clearly।
True or False
- API design applies only to HTTP APIs.
- A Java class's public methods form an API.
- More public methods always make a class easier to use.
- Strong domain types can reduce argument mix-ups.
- Every String should become a custom type.
- Generic setters can bypass domain invariants.
Optional<T>can clarify single-result lookup absence.Optional<List<T>>is always better than an empty List.- Returning an internal mutable List can leak ownership.
- Repository contracts should usually hide storage implementation details.
- Boolean parameters can sometimes make call sites unclear.
- Every operation needs a custom interface.
- Queries should avoid surprising major side effects.
- Failure semantics are part of API design.
- A good API makes correct use easier.
Answers
1. False
2. True
3. False
4. True
5. False
6. True
7. True
8. False
9. True
10. True
11. True
12. False
13. True
14. True
15. True
Knowledge Check
Question 1
Java code-এর context-এ API বলতে কী বোঝায়?
Question 2
Small public surface কেন useful?
Question 3
Information hiding কী?
Question 4
Strong parameter type কী advantage দেয়?
Question 5
Boolean blindness কী?
Question 6
কেন generic setter domain model-এ problematic হতে পারে?
Question 7
Optional<T> কখন useful return type?
Question 8
Empty collection কেন অনেক সময় Optional collection-এর চেয়ে better?
Question 9
Defensive copying কেন important?
Question 10
Leaky abstraction কী?
Question 11
Repository boundary কী hide করা উচিত?
Question 12
Command এবং Query-এর difference কী?
Question 13
Failure semantics কীভাবে API contract-এর অংশ?
Question 14
কেন over-general abstraction avoid করা উচিত?
Question 15
Good API design-এর primary goal কী?
Knowledge Check Answers
Answer 1
একটি class/component অন্য code-কে যে intentional operations এবং types expose করে, সেগুলো তার API।
Answer 2
Caller-এর available choices কম এবং clearer হয়, implementation change করা সহজ হয়, এবং invalid usage-এর সুযোগ কমে।
Answer 3
Caller-কে required capabilities দেওয়া কিন্তু internal implementation details hide করা।
Answer 4
Type system-এর মাধ্যমে domain meaning communicate করতে, wrong argument mix-up কমাতে এবং validation/normalization centralize করতে পারে।
Answer 5
Boolean argument দেখে caller যখন বুঝতে পারে না true বা false-এর business meaning কী।
Answer 6
Setter raw state mutation allow করতে পারে এবং valid lifecycle/invariants bypass করতে পারে।
Answer 7
যখন method একটি single value খোঁজে এবং value না পাওয়া একটি legitimate outcome।
Answer 8
Collection already zero elements represent করতে পারে।
Empty List natural no-results state।
Answer 9
Caller এবং component-এর mutable state ownership separate রাখতে এবং external mutation দিয়ে internal invariants ভাঙা prevent করতে।
Answer 10
যখন abstraction এমন implementation detail expose করে যা caller-এর জানার কথা নয়।
Example:
repository exposing Properties/file paths
Answer 11
Storage-specific concerns যেমন:
file format
SQL
HashMap
filesystem layout
যদি caller-এর responsibility না হয়।
Answer 12
Command state change করে।
Query information return করে।
Real APIs কিছু ক্ষেত্রে result-producing commands রাখতে পারে, কিন্তু purpose clear থাকা উচিত।
Answer 13
Caller জানতে পারে operation-এর possible outcomes কী এবং কীভাবে absence বা failure communicate হবে।
Answer 14
Every abstraction cognitive এবং maintenance cost যোগ করে।
Actual requirement ছাড়া generic abstraction unnecessary complexity তৈরি করতে পারে।
Answer 15
Correct use easy করা
incorrect use difficult করা
এবং intent clear করা।
API Design Checklist
একটি public method design করার সময় জিজ্ঞেস করুন:
Method name কি intent clear করে?
Parameter types কি meaningful?
Boolean flags avoid করা যায় কি?
Invalid input type system-এ reduce করা যায় কি?
Null policy clear কি?
Absence কীভাবে represented?
Failure কীভাবে represented?
Caller কি internal mutable state access করছে?
Return type কি correct semantics communicate করছে?
Implementation detail leak করছে কি?
এই method public হওয়া দরকার কি?
একই responsibility-এর API কি unnecessarily fragmented?
Caller কি domain language ব্যবহার করতে পারছে?
Core Mental Model
Good API design-এর core question:
Caller-কে কী জানা দরকার?
Not:
Implementation-এর সব capability কীভাবে expose করব?
For example:
Bad:
course.setStatus(
CourseStatus.PUBLISHED
);
Better:
course.publish();
Bad:
Course find(
String code
);
with undocumented null behavior।
Better:
Optional<Course> findByCode(
CourseCode code
);
Bad:
List<Lesson> lessons() {
return lessons;
}
Better:
List<Lesson> lessons() {
return List.copyOf(
lessons
);
}
The pattern is:
Expose intent.
Hide mechanics.
Protect invariants.
Lesson Summary
এই lesson-এ আমরা clear Java APIs এবং boundaries design করার foundation শিখেছি।
আমরা শিখেছি:
- Java classes-এর public methods-ও API
- API design মূলত contract design
- Public surface যত small এবং intentional হয়, reasoning তত সহজ
- Information hiding implementation details isolate করে
- Domain operations generic setters-এর চেয়ে stronger হতে পারে
- Strong types domain meaning এবং validation encode করতে পারে
- Primitive obsession argument ambiguity তৈরি করতে পারে
- Strong types meaningful হলেই ব্যবহার করা উচিত
- Boolean parameters call site-এ meaning hide করতে পারে
- Method names intent communicate করা উচিত
- Return type contract-এর গুরুত্বপূর্ণ অংশ
Optional<T>legitimate single-result absence represent করতে useful- Empty collection many-result absence-এর natural representation
- Null policies explicit হওয়া উচিত
- Validation boundary-তে করা useful
- Validated domain types downstream repeated checks কমায়
- Commands এবং Queries-এর purpose clear হওয়া উচিত
- Major side effects surprising হওয়া উচিত নয়
- Mutable collection ownership clearly controlled হওয়া দরকার
- Defensive copying encapsulation protect করে
- Repository APIs persistence implementation hide করা উচিত
- Over-general abstractions unnecessary complexity তৈরি করতে পারে
- Utility dumping grounds domain ownership hide করে
- Failure semantics API contract-এর অংশ
- Custom exceptions only meaningful distinction থাকলে useful
- Stable boundaries implementation change isolate করে
- Return interfaces/types actual semantic guarantees communicate করা উচিত
- Good API domain vocabulary-এর কাছাকাছি থাকে
সবচেয়ে important principle:
A good API does not expose
everything an object can technically do.
It exposes
what callers are meaningfully allowed to do.
আর practical rule:
Expose intent.
Hide implementation.
Protect invariants.
Make failure explicit.
Keep ownership clear.
Next Lesson
পরবর্তী নতুন lesson/module content:
Module 11 — Final Project
আমরা শুরু করব:
Designing the Final Project
Project:
LiveKlass Course Enrollment System
এখানে previous modules-এর concepts একসঙ্গে ব্যবহার করে একটি complete plain-Java application design করব:
Domain Model
Repository Contracts
Application Services
File Persistence
Console Application
Validation
Error Handling
Professional Package Structure