Professional Java Practices

Designing Clear APIs and Boundaries

ReadingPreview

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

  1. API design applies only to HTTP APIs.
  2. A Java class's public methods form an API.
  3. More public methods always make a class easier to use.
  4. Strong domain types can reduce argument mix-ups.
  5. Every String should become a custom type.
  6. Generic setters can bypass domain invariants.
  7. Optional<T> can clarify single-result lookup absence.
  8. Optional<List<T>> is always better than an empty List.
  9. Returning an internal mutable List can leak ownership.
  10. Repository contracts should usually hide storage implementation details.
  11. Boolean parameters can sometimes make call sites unclear.
  12. Every operation needs a custom interface.
  13. Queries should avoid surprising major side effects.
  14. Failure semantics are part of API design.
  15. 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